How-to: plugin
Register and manage Dynamics 365 plug-in assemblies, processing steps, and step
entity images via the Dataverse Web API (pluginassemblies, plugintypes,
sdkmessageprocessingsteps, sdkmessageprocessingstepimages).
See the CLI reference for every flag.
Register an assembly
crm --json plugin register-assembly ./bin/Contoso.Plugins.dll \
--solution cwx_contoso
The .dll bytes are base64-encoded and written to the content column of
pluginassemblies. --name defaults to the filename stem (Contoso.Plugins);
--version defaults to 1.0.0.0; --isolation-mode defaults to sandbox
(isolation mode 2). Pass --isolation-mode none for full-trust assemblies.
Re-upload assembly content after a rebuild
crm --json plugin register-assembly ./bin/Contoso.Plugins.dll --update --solution cwx_contoso
--update resolves the existing assembly by name and patches only the content
column — it does not touch name, version, culture, or publickeytoken.
Identity flags (--version, --culture, --public-key-token, --description,
--isolation-mode) are ignored under --update and produce a warning if passed.
--solution is still required (and honored).
Register a plug-in type
A content-only register-assembly does not create plugintypes rows — that
only happens when the Plug-in Registration Tool reflects the assembly client-side.
Assemblies uploaded via the CLI have zero type rows until you register each one
explicitly:
crm --json plugin register-type \
--assembly Contoso.Plugins \
--type Contoso.Plugins.PreCreateAccount \
--solution cwx_contoso
--friendly-name and --name both default to the type name.
version/culture/publickeytoken are read-only (server-derived from the
bound assembly) and are never sent in the request body. After this,
list-types shows the row and register-step --plugin-type <typename>
resolves it.
The name column (distinct from friendlyname) is what the classic
workflow designer shows as the Add-Step label for a workflow activity
(isworkflowactivity: true) — this is why it defaults to the fully qualified
type name (mirroring the Plug-in Registration Tool) rather than being left
null, which previously produced an empty, unusable label there. Pass
--name to give the label a different value instead.
Dry-run preview (no write): crm --dry-run --json plugin register-type ...
--solution cwx_contoso returns {_dry_run, would_create: true}. The assembly
name-to-id resolution GET runs live even under --dry-run (reads-execute rule);
--solution is still required and validated before it.
Unknown assembly name raises a D365Error (clean error, no server round-trip
for the write).
--solution lands the type row in a target solution and is required —
components must always target an explicit unmanaged solution. Pass
--solution Default for a deliberate Default-Solution-only write.
List plug-in types
For assemblies registered via the CLI, the listing is empty until each type is
registered with register-type (no platform reflection):
crm --json plugin list-types
crm --json plugin list-types --assembly Contoso.Plugins
Returned columns: typename, friendlyname, plugintypeid. Filter to a single
assembly with --assembly NAME.
Register a webhook
Webhooks are serviceendpoint rows (contract=8). The platform POSTs the JSON
execution context to the webhook URL whenever a registered step fires.
crm --json plugin register-webhook \
--name MyWebhook \
--url https://func.azurewebsites.net/api/d365hook \
--auth webhookkey \
--auth-value 'abc123secret' \
--solution cwx_contoso
Auth scheme choices: webhookkey (appends ?code=<auth-value> — the Azure
Functions default), httpheader (passes the value as an HTTP header),
httpquerystring (passes it as a query-string parameter). The auth value is
write-only: the platform never returns it on subsequent reads.
After registering the webhook, bind a step to it with --service-endpoint
(see "Register a processing step" below).
Dry-run preview (no write): crm --dry-run --json plugin register-webhook ...
--solution cwx_contoso carries meta.dry_run: true.
--solution lands the endpoint row in a target solution and is required.
Register a processing step
Bind to a plug-in type (--plugin-type) or a service endpoint such
as a webhook (--service-endpoint) — pass exactly one.
Bind to a plug-in type:
crm --json plugin register-step \
--message Update \
--plugin-type Contoso.Plugins.AccountPostUpdate \
--entity account \
--stage postoperation \
--mode sync \
--filtering-attributes name,telephone1 \
--configuration '{"key": "value"}' \
--solution cwx_contoso
Bind a step to a webhook (service endpoint):
crm --json plugin register-step \
--message Create \
--service-endpoint MyWebhook \
--entity account \
--stage postoperation \
--mode async \
--async-auto-delete \
--solution cwx_contoso
Key points:
- Exactly one of
--plugin-typeor--service-endpointmust be given — they are mutually exclusive; omitting or providing both is a usage error. --service-endpointmatches by the webhook (or other service endpoint) name given toregister-webhook. The step is bound via theeventhandler_serviceendpointnavigation property.--stagechoices:prevalidation(10),preoperation(20),postoperation(40). Default:postoperation.--modechoices:sync(0),async(1). Default:sync. Async mode requires--stage postoperation— other combinations are rejected.--async-auto-deleteconfigures an async step to delete its system job upon success.--configurationstores an unsecure configuration string on the step, passed to the plug-in constructor.--secure-configurationstores the secure configuration, passed to the plug-in constructor alongside the unsecure string. Unlike--configuration, the secure value lives in a separate related record (sdkmessageprocessingstepsecureconfig) created and linked to the step on registration. It is write-only per platform semantics — the value is never returned by the platform and is omitted from normal command output. A--dry-runpreview echoes the request body verbatim, so it does include the value; treat dry-run output as sensitive.--entitysets theprimaryobjecttypecode. Omit it to fire on all entities.--filtering-attributes(comma-separated) restricts an Update step to specific columns; ignored for non-Update messages.- Step name is auto-derived as
<handler>: <message> of <entity>(or<handler>: <message> of any entitywhen--entityis omitted), where<handler>is the plug-in typename or service endpoint name. Pass--nameexplicitly when the derived string would exceed the platform's 256-character limit. --assemblyscopes the type lookup to a single assembly when multiple assemblies share a type name (relevant only with--plugin-type).--solutionlands the step row in a target solution (setsMSCRM.SolutionUniqueName) and is required — there is no profile default; pass--solution Defaultfor a deliberate Default-Solution-only write.
Full registration workflow
Plug-in assembly → step
# 1. Upload the assembly
crm --json plugin register-assembly ./bin/Contoso.Plugins.dll --solution cwx_contoso
# 2. Register each IPlugin class explicitly — the CLI does not reflect the
# assembly, so plugintype rows are NOT created automatically.
crm --json plugin register-type \
--assembly Contoso.Plugins \
--type Contoso.Plugins.AccountPostUpdate \
--solution cwx_contoso
# 3. Register a post-operation sync step on account Update
crm --json plugin register-step \
--message Update \
--plugin-type Contoso.Plugins.AccountPostUpdate \
--entity account \
--stage postoperation \
--mode sync \
--filtering-attributes name,telephone1 \
--configuration '{"key": "value"}'
Webhook → step
# 1. Register the webhook endpoint
crm --json plugin register-webhook \
--name MyWebhook \
--url https://func.azurewebsites.net/api/d365hook \
--auth webhookkey \
--auth-value 'abc123secret' \
--solution cwx_contoso
# 2. Bind an async step on account Create to the webhook
crm --json plugin register-step \
--message Create \
--service-endpoint MyWebhook \
--entity account \
--stage postoperation \
--mode async \
--async-auto-delete \
--solution cwx_contoso
Register a step image
Step entity images snapshot the record before (pre) or after (post) the
core operation; plug-in code reads them from PreEntityImages /
PostEntityImages under the alias:
crm --json plugin register-image \
--step "Contoso.Plugins.AccountPostUpdate: Update of account" \
--type pre \
--alias preimg \
--attributes name,telephone1 \
--solution cwx_contoso
Key points:
--stepaccepts the step GUID or its exact name (an ambiguous name errors — use the GUID).--aliasis the key your plug-in uses to read the image;--namedefaults to the alias.--attributes(comma-separated) limits the columns captured in the image. Omitting it captures all columns — a documented performance anti-pattern; always pass a list.messagepropertynameis derived from the step's message automatically (Targetfor Assign/Delete/Merge/Route/Update,Idfor Create — the platform rejectsTargeton Create —EmailIdfor DeliverIncoming/DeliverPromote,EntityMonikerfor SetState).Sendsteps are ambiguous (FaxId,EmailId, orTemplateId) and require an explicit--message-property-name; messages outside that table do not support images and are rejected client-side.- Platform validity rules are enforced before any write, identically whether
the image type is
pre,post, orboth(abothimage counts as both a pre- and a post-image for these checks): no pre-image on aCreatestep, no post-image on aDeletestep, and post-images require a step registered in the PostOperation stage. --solutionlands the image row in a target solution and is required (same semantics as onregister-stepandregister-assembly).
Unregister a step image
crm --json plugin unregister-image preimg --yes
Resolves by image name or GUID; an ambiguous name errors — use the GUID. Deleting a step cascades its images automatically, so this is only needed to remove an image while keeping the step.
Set step state
crm --json plugin set-step-state "Contoso.Plugins.AccountPostUpdate: Update of account" --disable
crm --json plugin set-step-state "Contoso.Plugins.AccountPostUpdate: Update of account" --enable
Resolves by step name or GUID; an ambiguous name errors — use the GUID.
Under --dry-run the step-resolution GET runs live but no state change is written; the preview carries the standard dry-run markers rather than a fabricated updated: true: {_dry_run, would_set_state: {statecode, statuscode}, sdkmessageprocessingstepid}.
Unregister a step
crm --json plugin unregister-step "Contoso.Plugins.AccountPostUpdate: Update of account" --yes
Resolves by step name or GUID. An ambiguous name (multiple steps sharing the same
name) errors — use the GUID instead. --yes skips the interactive confirmation.
Unregister an assembly (cascading)
crm --json plugin unregister-assembly Contoso.Plugins --yes
Deletes all dependent steps first, then removes the assembly. Pass --yes to
skip confirmation. Accepts either the assembly name or its GUID.
Dry-run preview
crm --dry-run --json plugin register-assembly ./bin/Contoso.Plugins.dll --solution cwx_contoso
crm --dry-run --json plugin register-type \
--assembly Contoso.Plugins --type Contoso.Plugins.AccountPostUpdate --solution cwx_contoso
crm --dry-run --json plugin register-webhook \
--name MyWebhook --url https://func.azurewebsites.net/api/d365hook \
--auth webhookkey --auth-value 'abc123secret' --solution cwx_contoso
crm --dry-run --json plugin register-step \
--message Create --plugin-type Contoso.Plugins.AccountPreCreate --entity account \
--solution cwx_contoso
crm --dry-run --json plugin register-step \
--message Create --service-endpoint MyWebhook --entity account --solution cwx_contoso
Resolution GETs (e.g. assembly lookup under --update) fire for real; all
writes are skipped. --solution is still required and validated before any of
this (including under --dry-run). The --json envelope carries
meta.dry_run: true.
For register-step, dry-run resolves the objects the step names — the SDK
message, the plug-in type or service endpoint, and (when --entity is given)
the message filter for that entity — and reports each under
data.references[] = {kind, value, _exists}. A reference that does not resolve
keeps the preview non-failing (ok: true) and adds a meta.warnings advisory
naming it, so a bad message, unregistered type/endpoint, or unsupported entity
is caught before the real write 400s.