How-to: apply
crm apply -f spec.yaml stands up a whole custom table — publisher, solution,
entity, columns, option sets, relationships, views, web resources, security
roles, and plug-in assemblies with their types, steps, and images — from a
single declarative spec. It orchestrates the existing metadata and plug-in
commands in dependency order (publisher → solution → entities → option sets →
attributes → relationships → views → web resources → security roles → plug-ins)
and runs PublishAllXml once at the end — but only when a publishable
component was created or updated. Security roles and plug-in components are not
publishable customizations, so an apply that touches only those does not
publish.
apply is convergent: a component that already exists is reconciled against
the spec rather than blindly skipped. Three outcomes per component:
- equal — spec matches live definition →
skipped(idempotent re-apply). - updatable divergence — an in-place-editable field drifted → updated in place,
counted as
updated. Updatable fields: entity display name / display-collection name / description, and enablinghas_notes/has_activities(false → true); attribute display name, description, required level, and stringmax_lengthgrowth (shrinking is out of scope); adding declared options to a global option set; relationship cascade configuration, associated-menu (label / behavior / order), andis_hierarchical, plus the relationship-backed lookup column's display name, description, and required level (surfaced as one mergedupdatedentry per relationship block); viewdescription,is_default,columns,filter_active,order_by,order_desc(reconciled by record PATCH of regenerated fetchxml / layoutxml). - immutable/destructive divergence — the change cannot be made without
dropping the component →
replace_blocked: reported, no write for that component, run endsok=false(exit 1). Blocked cases: entity ownership change; explicithas_notes/has_activitiesdisable (true → false— enable-only; platform forbids disabling);is_activitychange (identity); attribute data-type change; relationship type mismatch or a referenced/referencing-entity or lookup-column change.
Create-only vs. reconciled spec keys. The full builder keyword surface is expressible in the spec. Keys that are reconciled on re-apply (drift is detected and updated in place):
- Relationship —
cascade_assign,cascade_delete,cascade_reparent,cascade_share,cascade_unshare,cascade_merge(cascade configuration);menu_label,menu_behavior,menu_order(associated-menu);is_hierarchical; and the relationship-backed lookup column'slookup_display,lookup_description,required(reconciled via the referencing attribute — surfaced as one mergedupdatedentry per relationship block).- Entity —
has_notesandhas_activities(false → trueonly — enable-only capability; the platform forbids disabling, so an explicittrue → falseisreplace_blocked); display name, display-collection name, and description (see the updatable bullet above).is_activitydivergence isreplace_blocked(identity change). Only spec-declared fields drift; omission never blanks.- View — an existing saved view matched by
(entity, name, query_type)is reconciled in place:description,is_default,columns(regenerateslayoutxml),filter_active,order_by,order_desc(regeneratefetchxml). A changednameorquery_typehas no live match and falls to the create path (a new view is made; the old one is left for--prune) — a documented limitation. An ambiguous match (>1 live view sharing the identity tuple) isskippedwith a reason rather than patching an arbitrary row.Keys that take effect at CREATE only (re-applying an existing component does not yet reconcile them):
- Attribute —
default_value,true_label/false_label,min_value/max_value,max_size_kb,auto_number_format,behavior_name,relationship_schema,is_audit_enabled.- Entity —
primary_attr_max_length,data_provider_id,data_source_id,external_name,external_collection_name,is_audit_enabled.- Relationship —
is_audit_enabled(on the backing lookup column).
export-specemits the subset of these keys that map to live Web API fields (the flatcascade_*/menu_*/is_hierarchical/lookup_description, viewfilter_active/order_desc, attributeauto_number_format/min_value/max_value/behavior_name/max_size_kb, entityhas_notes/has_activities/primary_attr_max_length), omitting platform defaults, so a create-side round-trip is lossless for those.
Reconciliation also runs under --dry-run, read-only: it reads the live org
while the reads-execute rule suppresses every write, so a dry-run reports the
full drift — planned (would create), updated (would update, each entry
carrying a field-level diff), replace_blocked, and pruned (solution
components absent from the spec) — without issuing a write.
See the CLI reference for the flags.
Spec schema
The spec is YAML (JSON is also accepted — it is a YAML subset). Every section is optional; provide only what you want to create.
publisher:
unique_name: contosopub # required
friendly_name: Contoso Publisher
prefix: contoso # required — 2-8 alphanumerics, customizationprefix
option_value_prefix: 10000 # required — 10000-99999
solution:
unique_name: ContosoCore # required; created components land here
friendly_name: Contoso Core
version: 1.0.0.0
optionsets: # global (org-level) option sets
- name: contoso_priority # required, must include the publisher prefix
display_name: Priority # required
options:
- {value: 100000000, label: Low}
- {value: 100000001, label: High}
entities:
- schema_name: contoso_Project # required, PascalCase with prefix
display_name: Project # required
display_collection_name: Projects
ownership: UserOwned
primary_attr: {schema_name: contoso_Name, label: Name}
attributes:
- {kind: string, schema_name: contoso_Code, display_name: Code, max_length: 100}
- {kind: picklist, schema_name: contoso_Priority, display_name: Priority, optionset_name: contoso_priority}
- {kind: lookup, schema_name: contoso_Owner, display_name: Owner, target_entity: systemuser}
relationships:
- schema_name: contoso_project_task
referenced_entity: contoso_project # the "1" side
referencing_entity: contoso_task # the "many" side
lookup_schema: contoso_ProjectId
lookup_display: Project
views:
- {name: Active Projects, columns: [contoso_name, contoso_code]}
forms: # converge the entity's main form (ADR 0024)
- # name omitted — targets the entity's primary main form
tabs:
- name: contoso_details # required; logical tab name (scripts bind to it)
label: Details # optional (defaults to name)
columns: 2 # optional, 1-4
sections:
- name: contoso_info
label: Info
fields:
- {name: contoso_code, label: Code} # attribute logical name
libraries: [contoso_/scripts/project.js] # web-resource names
handlers:
- {event: onload, function: Contoso.onLoad, library: contoso_/scripts/project.js}
- {event: onchange, field: contoso_code, function: Contoso.onCode, library: contoso_/scripts/project.js}
webresources:
- name: contoso_/scripts/project.js # required, unique name (must include publisher prefix)
file: scripts/project.js # required, path relative to the spec file
display_name: Project Script # optional
# webresourcetype omitted — inferred from .js extension
security_roles:
- name: Contoso Project Manager # required, role display name (key)
# business_unit omitted — defaults to the caller's business unit
privileges:
- access: [read, write, create]
entities: [contoso_project, contoso_task]
depth: deep
- access: [read]
all_entities: true
depth: basic
- privilege_names: [prvReadSystemForm]
depth: global
plugins:
- assembly: Contoso.Plugins # optional; defaults to the DLL's file stem
file: bin/Contoso.Plugins.dll # required; resolved relative to the spec file
isolation_mode: sandbox # optional (none|sandbox), default sandbox
version: 1.0.0.0 # optional override, default 1.0.0.0
# culture / public_key_token / description are optional overrides
types:
- type_name: Contoso.Plugins.AccountHandler # required; fully-qualified class (the key)
friendly_name: Account Handler # optional
steps:
- name: Contoso Account Handler # required; unique, stable convergent key
message: Create # required; SDK message (e.g. Create, Update)
plugin_type: Contoso.Plugins.AccountHandler # required; a registered type (declare it under types to register it)
entity: account # optional; entity scope (omit = message-level)
stage: postoperation # optional (prevalidation|preoperation|postoperation), default postoperation
mode: sync # optional (sync|async), default sync
rank: 1 # optional, default 1
filtering_attributes: name,... # optional; only meaningful on Update
configuration: "..." # optional unsecure config
images:
- alias: PreImage # required; the key within the step
image_type: pre # required (pre|post|both)
attributes: name,... # optional; comma-separated logical names
message_property_name: Target # optional override
apps: # top-level: model-driven apps (ADR 0024)
- name: Contoso Projects # required; app display name
unique_name: contoso_projects # required; publisher-prefixed identity
description: Project management # optional
components: # optional; record-backed components to bind
- {kind: view, id: 00000000-0000-0000-0000-000000000000} # kind: view|chart|form|dashboard|bpf|sitemap
sitemap: # optional; navigation, replaced wholesale
areas:
- id: contoso_area
title: Projects # optional (defaults to id)
groups:
- id: contoso_group
title: Delivery
subareas:
- {entity: contoso_project, title: Projects} # tables reach the app here
attributes[].kind is any kind metadata add-attribute accepts (string,
memo, integer, bigint, decimal, double, money, boolean,
datetime, picklist, multiselect, image, file, lookup). A picklist
needs optionset_name (a global set, usually declared under optionsets) or
inline options; a lookup needs target_entity. max_length is optional for
string/memo (defaults to 100 / 2000 when omitted) and rejected on any other kind.
source_type (simple / calculated / rollup) and formula_definition (XAML
string) layer a rollup or calculated column on top of a supported kind — mirroring
metadata add-attribute --type. source_type: calculated or rollup requires
formula_definition and is rejected on lookup/customer kinds. Omitting
source_type (or source_type: simple) creates a plain column. The reconcile
pass does not compare formula_definition — drift in the formula is not
detected or updated.
View columns are entity
logical names; use name:width (or {name, width}) to set a column width
(default 100). Malformed input is rejected up front, before any HTTP call.
webresources[].webresourcetype is an integer (1=HTML, 2=CSS, 3=JS, 4=XML,
5=PNG, 6=JPG, 7=GIF, 8=XAP, 9=XSL, 10=ICO, 11=SVG, 12=RESX). When omitted, the
type is inferred from the file extension. The web resource body comes from either:
file— a path resolved relative to the spec file's directory (type inferred from extension whenwebresourcetypeis omitted), orcontent— an inline base64 string (emitted bysolution export-spec; when using this formwebresourcetypeis required, as there is no extension to infer from).
Exactly one of file or content must be present. Web resources are published by the
end-of-run PublishAllXml (deferred by --stage-only). Convergent: unchanged content →
skipped; content or display name drift → updated.
security_roles[].privileges is a list of grant rows that are merged into the
declared set (highest depth wins per privilege). Each row specifies depth
(basic/local/deep/global) and either:
access(list of actions:read,write,create,delete,append,appendto,assign,share) withentities(list of logical names) orall_entities: true, orprivilege_names(list of privilege names likeprvReadAccount).
Convergent: skipped when every declared privilege is already present at its declared depth; on drift, replaced to the declared set (extras dropped).
Security role privileges: platform baseline and removal-only no-op
Dataverse automatically grants every role a set of immovable baseline
privileges (e.g. SharePoint document management, prvReadSharePointData). These
cannot be removed via ReplacePrivilegesRole. "Exactly the declared set" means
the declared privileges at their declared depths plus those immovable platform
privileges — apply will not report a replace_blocked for them.
A privilege dropped from the spec is only removed if some other declared
privilege also drifts in the same run (triggering a fresh replace). A
removal-only change — where all remaining declared privileges are already
satisfied — is a convergent no-op. To force a remove-only reconciliation, make a
no-op edit to another privilege in the role (e.g. increment and reset a depth),
or use crm security set-role-privileges directly.
Forms: converge the entity's main form
A forms: block is nested under an entity and converges that entity's
platform-generated main form (ADR 0024) — apply never forges a form from
scratch. The platform auto-creates a valid main form on entity create; the block
layers its declared structure onto that form and commits one formxml PATCH:
name(optional) selects among the entity's main forms; omitted, the primary main form is used. A declarednamemust match an existing main form.tabs[]— each needs aname(the logical name scripts bind to);labelandcolumns(1-4) are optional.sections[]nest under a tab (samename/label/columnsshape).fields[]nest under a section; each needs the attribute's logicalnameand takes an optionallabel(the controlclassidis resolved from the attribute's type).libraries[]— JS web-resource names to register on the form (each must already exist as a web resource; declare it underwebresources:in the same spec to seed it first).handlers[]— event wiring. Each needsevent(onload/onsave/onchange),function, andlibrary; anonchangehandler also needs thefieldit fires on (and onlyonchangetakes afield).pass_context(default true) andenabled(default true) are optional.
Convergence is additive and idempotent: a declared tab / section / field /
library / handler that is absent is added; one already present but drifted is
converged in place and counted updated with a field-level diff; one that already
matches is left untouched, so re-applying an unchanged spec reports the form
skipped. The drift converged in place: a tab / section label, a field's
tab + section placement (the existing control is relocated, not duplicated), the
relative order of the declared tabs / sections, and a handler's enabled /
pass_context flags. A field's control classid is create-only — a live
control type that diverges from the attribute is never retyped in place.
An identity / ownership divergence is refused with no write (replace_blocked,
exit 1, siblings still reconcile): a declared name that does not resolve to a
single existing main form. The stance is converge an existing main form — apply
never creates a named form from scratch or rewrites the wrong one.
--dry-run classifies the form planned (a greenfield entity's main form not yet
materialised is also planned) or, when only drift is present, updated with the
would-converge diff. Forms publish with the rest of the customization at the
end-of-run PublishAllXml, and --stage-only defers that publish like every other
kind. Forms are out of scope for --prune.
Model-driven apps: create the app, then converge it
A top-level apps: block declares a model-driven app (ADR 0024). An app that does
not yet exist is created through the same app-module and sitemap builders the
crm app verbs use, its declared components bound and its sitemap set, so one
spec can stand up a table and the app that exposes it in a single apply. An app
that already exists is reconciled (#796) against the declared block:
nameandunique_nameare required;unique_nameis the publisher-prefixed identity (the convergent key) anddescriptionis optional.components[]— record-backed components to bind viaAddAppComponents. Each is{kind, id}wherekindis one ofview,chart,form,dashboard,bpf,sitemapandidis the component's GUID. Tables are not bound here — they reach the app through a sitemap subarea (entity:below).sitemap— navigation asareas[] → groups[] → subareas[]. An area/group takes anidand optionaltitle(defaults to the id); a subarea takes anentity(logical name) and optionaltitle. The sitemap is auto-linked to the app by its unique name and set from the declared document wholesale.
Reconcile, on re-apply:
- Component set — only view/chart/form/dashboard/bpf components are diffed
(the app's sitemap component and its implicit table bindings are never touched
here). A component declared in
components[]but not bound on the live app is added (AddAppComponents); a component the live app binds but the spec no longer declares is removed (RemoveAppComponents). - Sitemap — converges by whole-document replacement: if the declared
sitemap XML differs from the live sitemap's, the live
sitemapxmlis PATCHed wholesale. An app with no linked sitemap yet gets one created. - Verdicts — an app that already matches the declared block is
skipped(idempotent re-apply); any component or sitemap change isupdated, and the entry carries acomponents: [{kind, id, change: "added"|"removed"}]list and/or asitemap: "converged"|"added"field. A managed app is refused with no write —replace_blocked, run exits 1, siblings still reconcile — since its components and sitemap are owned by its parent solution. --dry-runreads the live app and reports the full drift asupdated(component and sitemap changes included), with every converge write suppressed.
--dry-run on an absent app classifies it planned and issues no write. Apps and
sitemaps are publishable, so a created or updated app publishes with the rest of
the customization at the end-of-run PublishAllXml, and --stage-only defers
that publish. A created app that has a sitemap bound is also app-published
individually (PublishAllXml does not publish appmodules) so it is immediately
GET-visible in the appmodules collection rather than an invisible orphan; a bare
app (no sitemap) can't pass app-scoped publish validation and is left unpublished.
An updated (already-existing) app is not re-app-published. Malformed app/sitemap
blocks are rejected up front, before any HTTP call. Apps are out of scope for
--prune. App unique_name identity is not changed in place; app existence
(create vs. reconcile) is the only thing this block decides between.
Plug-ins: assembly, types, steps, and images
plugins[] declares one or more plug-in assemblies. Each entry identifies a
built DLL (file, resolved relative to the spec file) and optionally names the
assembly, its isolation mode, and its version override. Under it you declare the
types[] (the IPlugin classes) and steps[] (SDK message processing steps)
that should exist.
Convergent reconcile per component:
- Assembly — created when absent; the DLL content is PATCH-updated when a
rebuilt binary differs from the live assembly; skipped when content is
identical. The assembly name (or file stem if
assembly:is omitted) is the convergent key. - Type — registered when the declared
type_nameis not already present; skipped when it is.type_nameis immutable once registered. - Step (keyed by the unique
name) — created when absent; runtime config fields (stage,mode,rank,filtering_attributes,configuration) are updated in place when they drift. Only spec-declared fields are reconciled. A binding change — a differentmessage,entity, orplugin_typeon an existing step — is classifiedreplace_blocked: reported, no write, run exits - The platform fixes bindings at creation; updating them requires a
delete-and-recreate that
applydoes not perform automatically. - Image (keyed by step +
alias) — registered when absent; skipped when already present.
Plug-in components are not publishable, so a plugins-only apply does not issue
PublishAllXml. --dry-run is fully supported: a greenfield spec reports
components as planned; drift reports updated (with field-level diff) or
replace_blocked.
On-prem metadata writes are synchronous, so a single apply registers a new assembly, its types, and its steps in one pass. On Dataverse (cloud) a newly-registered plug-in type can take a few seconds to become queryable, so a single apply that both registers a new type and a step binding to it may report the step as
failed(the type is not yet resolvable); re-apply once it has propagated and the step lands (the already-created assembly and type are skipped). On-prem is the plug-in extensibility target.
Stand up a table in one shot
crm --json apply -f project.yaml
Output is {ok, data:{applied, updated, skipped, replace_blocked, pruned, planned, failed}, meta:{staged}}.
Each entry is {kind, name}; failed and replace_blocked entries also carry
error / reason. A re-apply of an unchanged spec reports all matching
components under skipped. A re-apply of a changed spec reports in-place edits
under updated; immutable divergences under replace_blocked (and exits 1).
pruned entries carry {kind, name, deleted} (plus reason when a data-bearing
component is refused without --allow-data-loss, plus would_prune: true under
--dry-run). pruned is populated under --dry-run (candidates, deleted: false)
and --prune (deletions); a plain real-run apply with neither leaves it empty.
Preview a greenfield spec
crm --dry-run --json apply -f project.yaml
Dry-run reports everything that would be created under planned and makes no
changes. Dependents of a not-yet-created resource (a column on a new table, a
picklist on a new option set, a solution on a new publisher) are reported
planned too, instead of erroring.
A brand-new table's
ObjectTypeCodeis often not readable until the apply's final publish, so its views land asplanned. Runapplya second time after the first publish to create them.
Save a drift report as a plan artifact
-o/--plan-out <path> serializes the --dry-run drift report to a plan — a
self-contained JSON artifact you can review, attach to a PR or ticket, and later
execute with --from-plan (below). It is valid only with the global
--dry-run flag (a usage error, exit 2, otherwise).
crm --dry-run --json apply -f project.yaml -o project.plan.json
The plan is written on every dry-run, including one that exits 1 because a
component is replace_blocked — the plan doubles as the drift-report artifact,
and the exit code is unchanged. A plan captures:
- a header — plan-format version, the target Web API base URL and
organizationid(from WhoAmI), the solutionunique_name, the CLI version, a timestamp, and the plan intent (prune,allow_data_loss,stage_onlyas passed at plan time); - the resolved spec embedded verbatim (not a path reference);
- payload pins — a
sha256per referenced file payload (web-resource bodies, plug-in assembly DLLs), pinning content without inlining it; and - verdict records — one per component: its
kind,name,verdict(planned/updated/skipped/replace_blocked/pruned/failed), and the field-leveldiffwhere the engine computes one.
Verdict records cover every apply kind — schema, automation, and the UI kinds
(forms, and model-driven apps + their sitemaps, ADR 0024) alike, since kind is a
free string. An updated form carries its per-component converge diff; an
updated app carries a changed-field set of its component-add/remove and sitemap
actions — so a whole customization (schema + UI) rides one approval-gated plan.
Forms and apps stay out of --prune (ADR 0024), so a pruned record never
names one.
The command's own JSON envelope also reports the written path in meta.plan_out.
Execute a plan — approval-gated apply
--from-plan <path> runs a saved plan, but only if it is still exactly true.
This closes the approval gap: what you reviewed in the plan is what runs, or
nothing runs. It is mutually exclusive with -f (exactly one is required).
# 1. plan (review plan.json, attach it to a PR/ticket, get it approved)
crm --dry-run --json apply -f project.yaml -o project.plan.json
# 2. re-verify without executing — the CI pre-check
crm --dry-run --json apply --from-plan project.plan.json
# 3. execute the approved plan
crm --json apply --from-plan project.plan.json
Plan intent is replayed, not re-specified. --prune, --allow-data-loss,
and --stage-only are fixed when the plan is created and read back from its
header — passing any of them (or -o) alongside --from-plan is a usage error
(exit 2). The destructive confirmation (--yes / TTY prompt) still applies when
the plan carries prune intent.
Pre-flight refusals (exit 1, no writes):
- an unknown/newer
plan_formatthe CLI can't read; - an
organization_idthat doesn't match the connected org's WhoAmI (a mismatched URL or CLI version is ameta.warningsnote, not a refusal — hostnames may be aliased); - a plan carrying
replace_blocked/failedcomponents (the clean-plan rule — such a plan approves an outcome apply will never converge to); - any pinned payload that is missing or whose content changed since plan time.
Payload
filepaths are resolved relative to the plan file's directory, so keep referenced web-resource/DLL files next to the plan.
The whole-run gate. Execution first recomputes the drift report from live
reads and compares it to the plan at the action level — the component set,
each verdict, and each updated component's changed-field set must match exactly
(live field values need no byte equality). Any divergence is a stale plan:
zero writes, ok=false, exit 1, with each diverged component reported under
data.divergences as "plan said X, live now computes Y". The remedy is always to
re-plan and re-approve. Without prune intent, a stray new solution component that
surfaces live is informational and does not invalidate the plan; under prune
intent the approved deletions participate in the gate. A live UI edit between
plan and apply — a form's layout or an app's sitemap changed out of band — shifts
that component's recomputed action and is caught by the same gate.
--dry-run --from-plan stops after the compare (verify mode): it reports
data.plan_valid (true/false) and writes nothing — use it as a CI gate that a
pending plan is still applicable.
TOCTOU note. A residual window survives between the verify pass and the writes — metadata writes are not transactional — so a concurrent customization could still slip in after the gate passes. The gate shrinks the window from preview-to-apply to verify-to-write; it does not eliminate it.
Stage without publishing
crm --stage-only --json apply -f project.yaml
--stage-only creates every component but skips PublishAllXml; meta.staged
is true. Publish later with crm solution publish.
Partial failure
Metadata POSTs are not transactional. If a step fails, apply aborts the
remaining steps, reports the failure under failed (with the error), exits
non-zero, and does not publish. Whatever was already created is left
staged-but-unpublished (meta.staged is true) — fix the spec and re-apply;
the already-created resources are skipped.
Prune org-extras
--prune opts in to solution-bounded deletion of components that are members of
the target solution but are no longer declared in the spec. A plain apply
never reads solution members and never deletes anything; pruning is entirely
opt-in.
Eligible kinds (six): entity, attribute, view, security-role,
webresource, plugin-step. Every other solution component type (option sets,
relationships, plug-in assemblies/types, forms) is out of scope.
Gating:
- Schema-only extras (
view,security-role,webresource,plugin-step) are deleted on--pruneafter confirmation. - Data-bearing extras (
entity,attribute) destroy row data and are refused unless--allow-data-lossis also passed. Without it they appear inprunedwithdeleted: falseandreason: "data-bearing; pass --allow-data-loss to delete". - Under
--jsonor a non-TTY, a real--prunerequires--yes(no interactive prompt); a--dry-runpreview deletes nothing and needs no confirmation. --pruneis scoped to the spec's mandatory top-levelsolution:block — every spec must declare one (see "Spec schema" above), so a target solution is always in place.- Pruning is suppressed when the convergence phase itself has failures or replace-blocked components — a partial-failure run does not also delete org-extras.
Always preview first:
crm --dry-run --json apply -f project.yaml --prune
Dry-run reports all prune candidates under pruned with deleted: false; those
that would actually be deleted carry would_prune: true. No write is issued.
Pruning never triggers a publish.
Worked example — remove a stale web resource and view from the solution:
Spec project.yaml declares solution: {unique_name: ContosoCore} and
previously declared contoso_/scripts/old.js and a view Old Projects; both
are now removed from the spec. The solution still has them as members.
# 1. Preview what would be pruned
crm --dry-run --json apply -f project.yaml --prune
# 2. Apply with pruning (interactive confirmation on a TTY)
crm apply -f project.yaml --prune
# 3. Under CI / --json there is no prompt, so pass --yes
crm --json apply -f project.yaml --prune --yes
Example output (data.pruned):
[
{"kind": "webresource", "name": "contoso_/scripts/old.js", "deleted": true},
{"kind": "view", "name": "Old Projects", "deleted": true}
]
A data-bearing extra refused without --allow-data-loss:
{"kind": "attribute", "name": "contoso_legacycode", "deleted": false,
"reason": "data-bearing; pass --allow-data-loss to delete"}
Referenced global option sets
When a spec contains an optionsets block, apply automatically adds each
referenced global option set to the spec's solution: block target (via
AddSolutionComponent) even if the option set already existed and was skipped
during creation. This ensures option sets created in a previous apply run (or
pre-existing) are properly linked to the solution.
To opt out:
crm apply -f spec.yaml --no-include-referenced-optionsets
This flag is also exposed as include_referenced_optionsets on the Python
apply_spec function for programmatic callers.