Skip to content

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 enabling has_notes / has_activities (false → true); attribute display name, description, required level, and string max_length growth (shrinking is out of scope); adding declared options to a global option set; relationship cascade configuration, associated-menu (label / behavior / order), and is_hierarchical, plus the relationship-backed lookup column's display name, description, and required level (surfaced as one merged updated entry per relationship block); view description, 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 ends ok=false (exit 1). Blocked cases: entity ownership change; explicit has_notes / has_activities disable (true → false — enable-only; platform forbids disabling); is_activity change (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):

  • Relationshipcascade_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's lookup_display, lookup_description, required (reconciled via the referencing attribute — surfaced as one merged updated entry per relationship block).
  • Entityhas_notes and has_activities (false → true only — enable-only capability; the platform forbids disabling, so an explicit true → false is replace_blocked); display name, display-collection name, and description (see the updatable bullet above). is_activity divergence is replace_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 (regenerates layoutxml), filter_active, order_by, order_desc (regenerate fetchxml). A changed name or query_type has 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) is skipped with 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):

  • Attributedefault_value, true_label / false_label, min_value / max_value, max_size_kb, auto_number_format, behavior_name, relationship_schema, is_audit_enabled.
  • Entityprimary_attr_max_length, data_provider_id, data_source_id, external_name, external_collection_name, is_audit_enabled.
  • Relationshipis_audit_enabled (on the backing lookup column).

export-spec emits the subset of these keys that map to live Web API fields (the flat cascade_*/menu_*/is_hierarchical/lookup_description, view filter_active/order_desc, attribute auto_number_format/min_value/max_value/ behavior_name/max_size_kb, entity has_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 when webresourcetype is omitted), or
  • content — an inline base64 string (emitted by solution export-spec; when using this form webresourcetype is 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) with entities (list of logical names) or all_entities: true, or
  • privilege_names (list of privilege names like prvReadAccount).

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 declared name must match an existing main form.
  • tabs[] — each needs a name (the logical name scripts bind to); label and columns (1-4) are optional. sections[] nest under a tab (same name / label / columns shape). fields[] nest under a section; each needs the attribute's logical name and takes an optional label (the control classid is 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 under webresources: in the same spec to seed it first).
  • handlers[] — event wiring. Each needs event (onload / onsave / onchange), function, and library; an onchange handler also needs the field it fires on (and only onchange takes a field). pass_context (default true) and enabled (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 formapply 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:

  • name and unique_name are required; unique_name is the publisher-prefixed identity (the convergent key) and description is optional.
  • components[] — record-backed components to bind via AddAppComponents. Each is {kind, id} where kind is one of view, chart, form, dashboard, bpf, sitemap and id is the component's GUID. Tables are not bound here — they reach the app through a sitemap subarea (entity: below).
  • sitemap — navigation as areas[] → groups[] → subareas[]. An area/group takes an id and optional title (defaults to the id); a subarea takes an entity (logical name) and optional title. 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 sitemapxml is 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 is updated, and the entry carries a components: [{kind, id, change: "added"|"removed"}] list and/or a sitemap: "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-run reads the live app and reports the full drift as updated (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_name is not already present; skipped when it is. type_name is 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 different message, entity, or plugin_type on an existing step — is classified replace_blocked: reported, no write, run exits
  • The platform fixes bindings at creation; updating them requires a delete-and-recreate that apply does 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 ObjectTypeCode is often not readable until the apply's final publish, so its views land as planned. Run apply a 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 solution unique_name, the CLI version, a timestamp, and the plan intent (prune, allow_data_loss, stage_only as passed at plan time);
  • the resolved spec embedded verbatim (not a path reference);
  • payload pins — a sha256 per 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-level diff where 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_format the CLI can't read;
  • an organization_id that doesn't match the connected org's WhoAmI (a mismatched URL or CLI version is a meta.warnings note, not a refusal — hostnames may be aliased);
  • a plan carrying replace_blocked / failed components (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 file paths 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 --prune after confirmation.
  • Data-bearing extras (entity, attribute) destroy row data and are refused unless --allow-data-loss is also passed. Without it they appear in pruned with deleted: false and reason: "data-bearing; pass --allow-data-loss to delete".
  • Under --json or a non-TTY, a real --prune requires --yes (no interactive prompt); a --dry-run preview deletes nothing and needs no confirmation.
  • --prune is scoped to the spec's mandatory top-level solution: 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.