Skip to content

How-to: workflow

Common crm workflow recipes. See the CLI reference for every flag.

List workflow definitions

crm --json workflow list --entity cwx_ticket --category 0
--category filters by kind. Accepts an integer (05) or a friendly name: workflow, dialog, businessrule, action, bpf, flow (case-insensitive). --on-demand restricts to on-demand workflows.

Find duplicate workflow definitions

After repeated solution imports (especially a failed import that gets retried), an org can accumulate multiple workflow definitions sharing one name. There is no dedicated flag for this — it is a client-side group-by over what list already returns:

crm --json workflow list \
  | jq '[.data | group_by(.name)[] | select(length > 1)
         | {name: .[0].name, count: length,
            rows: [.[] | {workflowid, statecode, statuscode}]}]'

Any list filter composes as a pre-filter, since the grouping runs over whatever list returns:

crm --json workflow list --entity cwx_ticket \
  | jq '[.data | group_by(.name)[] | select(length > 1)
         | {name: .[0].name, count: length,
            rows: [.[] | {workflowid, statecode, statuscode}]}]'

Why group over list output specifically: list is restricted to definitions (type=1). Activating any workflow makes the server spawn a same-name type=2 activation copy, so grouping raw workflow rows by name would flag every activated workflow as a duplicate. Because list excludes activation copies, every name this recipe surfaces is a genuine duplicate definition. parentworkflowid is not needed here — it is always null on definitions; its only use is distinguishing activation copies, which are already excluded.

Assess classic workflows for migration to cloud flows

Inventory category-0 (classic) workflow definitions and flag the ones that can't be rebuilt as a Power Automate cloud flow as-is:

crm --json workflow migration-assess
crm --json workflow migration-assess --entity account   # scope to one entity

Read-only. Each row reports verdict (ready/blocked) and a blockers list naming which of three rules fired, anchored to Microsoft's capability table:

  • real_time — the workflow runs synchronously (real-time); cloud flows can't.
  • wait_condition — the definition uses a wait/wait-timeout condition (a Postpone step); cloud flows can't.
  • custom_activity — the definition references a custom (non-out-of-box) workflow activity; cloud flows can't run them.

A blocked verdict means needs redesign, not impossible — Microsoft's guide lists a recommended pattern for each blocker (switch/looping actions, the recurrence trigger, changesets, custom connectors). A ready workflow uses only flow-capable primitives; background mode, on-demand, and child/subprocess workflows are all fine.

Cloud flows live only on Dataverse online. On an on-prem profile the command still runs and returns the same report, with an advisory meta.note reminding you the migration target must be an online environment:

{"ok": true, "data": [...], "meta": {"count": 10, "note": "Cloud flows don't exist on on-prem; the migration target must be a Dataverse online environment. This report assesses readiness only."}}

Filter the report client-side with jq, e.g. only the blocked ones:

crm --json workflow migration-assess \
  | jq '[.data[] | select(.verdict == "blocked")]'

Activate a workflow

crm --json workflow activate <workflow-guid>
Sets statecode=1, statuscode=2; crm workflow deactivate reverses it.

Deactivate a workflow

crm --json workflow deactivate <workflow-guid> --yes

Deactivation changes server-side state (sets statecode=0). Pass --yes when calling non-interactively — omitting it in a non-TTY context (e.g. --json or piped) fails fast (exit 1) with an error that names --yes — the standard ok:false envelope under --json, a human-formatted error otherwise. On an interactive terminal the verb prompts for confirmation instead.

If you pass an activation-record GUID (type=2 — the compiled copy the server creates when a draft is activated), the command resolves the parent definition via the row's parentworkflowid lookup and applies the state change to the parent automatically. The result carries a note naming both GUIDs (in human and --json output alike), so you can see the redirect happened:

{"ok": true, "data": {...}, "meta": {"note": "Operated on parent definition <parent-guid>; activation-record GUID <passed-guid> was passed."}}

Passing a draft GUID is unchanged: no note, and on a live run no extra round-trip. If the parent cannot be resolved, the command surfaces the server's original 0x80045003 rejection with a hint naming the parent definition GUID when known. Under --dry-run the resolution GET always runs (whichever GUID you pass), so the preview is keyed on the same GUID the live run would patch, and the payload carries the standard dry-run markers rather than a fabricated success:

{"ok": true, "data": {"_dry_run": true, "would_set_state": {"statecode": 1, "statuscode": 2}, "workflow_id": "<definition-guid>", "resolved_from_activation_id": null}}

crm entity delete workflows <guid> against that same activation-record GUID fails too — D365 rejects deleting activation rows directly (server code 0x80045004). You can't delete the activation; deactivate its parent definition instead, which removes the activation — or use crm workflow delete, which handles the whole sequence (see below). The error carries a hint: when the parent can be resolved it names the parent GUID and the exact crm workflow deactivate <parent-guid> command; otherwise it points you at the activation row's parentworkflowid lookup.

Delete a workflow definition

crm --json workflow delete <workflow-guid> --yes

Deletes the workflow definition (type=1), deactivating it first when it is active. Without --yes the confirmation prompt names the exact definition (name + GUID) the delete will hit.

Passing an activation-record GUID (type=2) works too: the command resolves the parent definition via the row's parentworkflowid lookup and operates on the parent — the server removes the activation record together with the definition. The prompt then says explicitly that you passed an activation record and names the definition it resolved to, and the result carries the same meta.note as activate/deactivate, naming both GUIDs.

Two failure modes worth knowing:

  • Not atomic. Deactivate-then-delete is two requests with no transaction. If the deactivate lands and the delete then fails, there is no rollback — the error states the definition was deactivated and remains a draft.
  • No live parent. An activation record whose parent definition is gone (null or dangling parentworkflowid) has no supported Web API delete path (ADR 0003, Web API only — no SOAP); the command fails cleanly and points you at the D365 UI. In practice this case has not been observed — see issue #164.

Under --dry-run the resolution GETs still run live, and the preview reports what would happen:

{"ok": true, "data": {"_dry_run": true, "would_delete": "<definition-guid>", "would_deactivate": true, ...}}

Update a workflow definition

Update metadata (name, scope, triggers)

crm --json workflow update <workflow-guid> --name "New display name"
crm --json workflow update <workflow-guid> --scope organization --on-demand
crm --json workflow update <workflow-guid> --on-update-attributes name,telephone1

Only the fields you pass are changed. Works on both on-premises and cloud targets. When the workflow is active, the command automatically deactivates it, applies the change, then reactivates it. Pass only the flags you need — the metadata path and the XAML path (--xaml-file) are mutually exclusive.

Replace workflow step logic with a new XAML file (on-premises only)

crm --json workflow update <workflow-guid> --xaml-file ./updated-logic.xaml

Replaces the workflow's entire step definition. On-premises only — on Dataverse (cloud) the command refuses before making any write, citing the provenance wall. The XAML is validated against the entity's live attribute set before writing; warnings surface on meta.warnings.

Options that apply only with --xaml-file:

  • --strict — promote any reference-validation warning to a hard failure; the write is skipped if any warning fires.
  • --rollback / --no-rollback — on reactivation failure, restore the prior XAML (default: --rollback). Use --no-rollback to leave the rejected XAML in place for inspection before deciding whether to fix or revert manually.
# Hard-fail on any validation warning
crm --json workflow update <workflow-guid> --xaml-file ./updated-logic.xaml --strict

# Inspect the rejected XAML if reactivation fails instead of rolling back
crm --json workflow update <workflow-guid> --xaml-file ./updated-logic.xaml --no-rollback

If you pass an activation-record GUID (type=2), both paths resolve the parent definition first. The --xaml-file path does whole-definition replace — it is not a fragment or step splice. To compose new XAML, author it in the D365 designer first, then export and edit the XAML out-of-band before feeding it back here.

Trigger an on-demand workflow against a record

crm --json workflow run <workflow-guid> --target <record-guid>
Calls ExecuteWorkflow against the target record; --as-user <guid> impersonates a systemuser via MSCRMCallerID, or --as-user-object-id <guid> impersonates an Entra ID user via CallerObjectId (cloud). The two are mutually exclusive.

Clone a workflow onto another entity

Duplicate a classic workflow or business rule onto a different entity. The clone gets a fresh GUID; the xaml is retargeted to reference the new entity.

# Clone and activate immediately (default)
crm --json workflow clone <workflow-guid> --to-entity cwx_ticketclone

# Clone as draft only
crm --json workflow clone <workflow-guid> --to-entity cwx_ticketclone --no-activate

# Custom name and add to a solution
crm --json workflow clone <workflow-guid> \
    --to-entity cwx_ticketclone \
    --name "Ticket Clone — Send notification" \
    --solution my_solution

Action and business-process-flow cloning is not yet supported; use solution export/import for those.

Under --dry-run the source definition is read live but nothing is written; the preview reports the clone that would be created:

{"ok": true, "data": {"_dry_run": true, "would_clone": {"source_id": "<src-guid>", "workflow_id": "<new-guid>", "name": "Ticket Clone", "primaryentity": "cwx_ticketclone", "category": 0, "activate": true, "solution": "my_solution"}}}

Export a workflow definition

Save the full workflow definition (including xaml) to a JSON file for source control or migration.

crm --json workflow export <workflow-guid> --output ./workflows/update-request.json

Import a workflow definition

Upsert a previously exported workflow definition back to an org.

# Import as draft (default)
crm --json workflow import --file ./workflows/update-request.json

# Import and activate immediately
crm --json workflow import --file ./workflows/update-request.json --activate

The workflowid in the JSON file is preserved — the import is an explicit-GUID upsert, so re-running is idempotent.