Blueprint Ownership: What an Update Keeps
A blueprint update re-imports the seed data into a tenant that has been in use for a while. The import is a full replace of every entity it touches, so without rules a seed that carries an empty value would erase what the tenant has entered since the install (an adapter configuration, an enforcement mode, a counter, a mail text). The rules on this page decide, for every value, whether the blueprint or the tenant owns it and what the platform does when the two disagree.
There are four layers. Each one is independent; a value is only replaced when none of them protects it.
| Layer | Question | Decided by |
|---|---|---|
| Seed entity ownership | Does the tenant own the whole entity? | rtBlueprintLocked in the seed |
| Attribute ownership | Does the tenant own this attribute? | ownership in the CK model |
| Blanking guard | Would the seed clear a value that is filled? | The engine, for every SeedOwned attribute |
| Preview and confirmation | What would the update keep or clear, and does the operator agree? | PreviewBlueprintUpdate, UpdateBlueprint |
An additional switch, ProtectBlueprintLocked, is not about updates but about users: it makes the entities a blueprint owns read-only for them.
The attributes that every platform and app blueprint writes, and the owner of each, are listed in the seed ownership audit and its full table.
The four ownership values
Every CK attribute declares who owns its value with ownership. The ownership is resolved per attribute assignment: an override on the type's assignment wins over the attribute definition, and an attribute without any declaration is SeedOwned. (isRuntimeState: true is the deprecated spelling of RuntimeState.)
ownership | Who owns the value | Fresh install | Blueprint update or forced re-apply | ExportRt |
|---|---|---|---|---|
SeedOwned (default) | The product. The blueprint author ships it and a new version may correct it. | Seed value lands | Seed value replaces the stored value, except that the blanking guard keeps a filled value when the seed is empty | Exported |
TenantOwned | The tenant. An administrator or user sets it in the product. | Seed value initialises it | Stored value is kept; the seed only initialises a new entity | Exported |
RuntimeState | A service, an operator or a pipeline writes it at runtime. | Seed value initialises it | Stored value is kept | Left out |
Secret | Credentials. Always the ownership of value type Secret. | Seed value initialises it (a Secret value type is empty in a seed) | Stored value is kept | Never exported |
attributes:
- id: PolicyEnforcementMode
valueType: Enum
valueCkEnumId: ${this}/DataPolicyEnforcementMode
ownership: TenantOwned
Examples from the platform models: System.Identity/DataPolicy PolicyEnforcementMode and PolicyScope are TenantOwned (an administrator who switched a policy to Enforce keeps Enforce); System.Communication/Adapter.AdapterConfiguration is Secret (the connection settings of an adapter, which can hold a password, survive an update and never travel in an export file); System.StreamData Archive.Status is RuntimeState.
Review question for every new or changed attribute: could an operator or user type this value in the product? If yes, it is TenantOwned (or Secret for a credential); if a service writes it, RuntimeState; only a value the product ships and must be able to correct is SeedOwned. Changing the ownership of an existing attribute is a minor version bump of the CK model.
RuntimeState and TenantOwned protect a tenant value, and the same protection stops a blueprint from correcting it. A seed fix of such a value reaches a fresh tenant and no existing tenant. Two cases are deliberate:
Adapter.ChartVersion(and the other deployment values of a workload) areRuntimeState: the operator or the CD pipeline sets them.- The redirect and CORS URIs of an identity
ClientareRuntimeState: the tenant or an overlay adds them.
A corrected value in either has to be written to the existing tenants by a CK migration step or a documented manual step, not by a new seed.
Tenant-owned seed entities
The flag rtBlueprintLocked in the seed says who owns an entity once it exists on the tenant:
| Seed says | Owner | First install | Update, tenant holds it | Update, tenant lacks it |
|---|---|---|---|---|
flag absent or true (default) | product | created | renewed from the seed (Merge, Full) | created again (a deleted entity comes back) |
rtBlueprintLocked: false | tenant | created with the seed values | not touched, no conflict (only the blueprint stamp moves) | created only if it is new; stays deleted if the previous version's seed already contained it |
entities:
- rtId: 65f1c0a2b3d4e5f601234567
ckTypeId: MyModel/MailTemplate
rtWellKnownName: DefaultTemplate
rtBlueprintLocked: false # created once, then the tenant's
attributes:
- id: Name
value: Default template
Use this for defaults a tenant is meant to edit or remove (templates, example rules). Keep the entities the product needs, such as the rules a feature depends on, locked. The preview and the update result list the entities in TenantOwnedSkipped (kept) and TenantOwnedStaysDeleted (not brought back); see below where to read these lists. A tenant that still carries the stamp rtBlueprintLocked: true from an earlier version is handled as tenant-owned from the first update whose seed declares the entity unlocked. ImportRt -r is not blueprint-aware and does not follow this rule; use UpdateBlueprint or InstallBlueprint to apply a seed.
The blanking guard
Attribute ownership only protects what a model author marked. For everything else (SeedOwned, the default), the engine decides per attribute on every Upsert import (blueprint install, update, forced re-apply, ImportRt -r):
| Stored value | Seed | Result |
|---|---|---|
| none, or empty | anything | seed value lands |
| filled | different, filled value | seed wins (a blueprint-owned correction, as before) |
| filled | empty value (null, "", empty list, record of empty members) | stored value kept, reported |
| filled | attribute omitted, no CK default | stored value kept, reported |
| filled and different from the CK default | attribute omitted, attribute has a CK default | stored value kept, reported as ResetToDefault |
| JSON text with a filled string | JSON text with an empty string at the same place | stored value kept as a whole, reported (the empty skeleton of an adapter configuration) |
"Empty" is narrow on purpose: numbers, booleans, enums, dates and time spans are never empty, so an explicit 0, false or the first enum member in a seed is a value the author chose and replaces the tenant's value. Records are kept or replaced as one unit.
The guard is a safety net, not a licence: a value a tenant types in belongs to TenantOwned or Secret in the model. The guard cannot tell a tenant-entered value from a seed value that a new version wants to clear, which is why it never clears silently; see the next section.
Preview and explicit confirmation
PreviewBlueprintUpdate runs the same detection as the update and lists every attribute whose filled value the seed would blank, without values (they can be credentials): entity id, type, attribute, reason (SeedEmpty, SeedOmitted, ResetToDefault; the CLI prints the reason as the service reports it) and a kind and size summary of the current and the incoming value, for example string (223 chars) against empty string.
# Read the list. The JSON result is written to standard output, the table is log output.
octo-cli -c PreviewBlueprintUpdate -tv MyBlueprint-2.0.0
# CI gate: exit code -5 when anything would be blanked
octo-cli -c PreviewBlueprintUpdate -tv MyBlueprint-2.0.0 -fb
# Default update: tenant values the seed would blank are KEPT and listed, exit code 0
octo-cli -c UpdateBlueprint -tv MyBlueprint-2.0.0
# Blank exactly these attributes (repeatable): <rtId>:<attribute> as listed by the preview
octo-cli -c UpdateBlueprint -tv MyBlueprint-2.0.0 -cb 65a1b2c3d4e5f60718293a4b:configuration
# Blank EVERY listed attribute (loud warning; prefer -cb)
octo-cli -c UpdateBlueprint -tv MyBlueprint-2.0.0 -ab
| Short | Long | On | Meaning |
|---|---|---|---|
-fb | --failOnBlanking | PreviewBlueprintUpdate | Exit with code -5 when the update would blank at least one tenant value |
-cb | --confirm-blanking | UpdateBlueprint | Confirm one attribute, <rtId>:<attribute>; repeat the option for several. Everything not confirmed is kept |
-ab | --allow-blanking | UpdateBlueprint | Confirm every listed attribute; takes precedence over -cb |
The full option lists are in the command reference of PreviewBlueprintUpdate and UpdateBlueprint. The REST API carries the same contract: POST {tenantId}/v1/blueprints/updates/preview returns blankedAttributes, and POST {tenantId}/v1/blueprints/updates/apply accepts allowBlanking and confirmedBlankings ([{ rtId, attributeName }]) and answers with the list again, where appliedOnUpdate is false for a kept value and true for one that was blanked on confirmation. Nothing is silent.
What to do with the list. For each line, decide who is right. A value the tenant entered and the product should not touch: do nothing (the update keeps it) and report the attribute as a model problem, because it should be TenantOwned. A value the product really wants to remove: confirm it with -cb. Studio applies updates and re-applies with the default, so it never blanks; it does not display the list yet, use PreviewBlueprintUpdate before an update on a tenant with entered data.
A preview and a guard are only as good as the service that runs them: a service older than the feature reports no list and behaves as before (see Availability).
Where operators see skipped and stays-deleted entities
An update leaves tenant-owned seed entities alone on purpose. So that this is visible and not a silent omission, the preview and the update result list them, in REST, GraphQL, the SDK and octo-cli. They are not blanking: blankedAttributes, -cb, -ab and -fb do not look at them.
| List | Meaning | entityId |
|---|---|---|
tenantOwnedSkipped | The tenant still holds the entity; the update left it untouched | Runtime id on the tenant |
tenantOwnedStaysDeleted | The previous seed contained it, the tenant deleted it, the update does not bring it back | null |
Each entry carries key (the seed entity's rtWellKnownName, else its rtId), ckTypeId, entityId and wellKnownName. Identity only, never attribute values: a tenant-owned entity can hold credentials. Both lists are counted in entitiesSkipped and are empty when there is nothing to report; a client that ignores them behaves as before.
| Surface | Where |
|---|---|
| REST | POST {tenantId}/v1/blueprints/updates/preview (tenantOwnedSkipped, tenantOwnedStaysDeleted in the JSON body) and POST {tenantId}/v1/blueprints/updates/apply (same members in the result) |
| GraphQL | blueprints.previewUpdate { tenantOwnedSkipped { key ckTypeId entityId wellKnownName } tenantOwnedStaysDeleted { ... } } and the same fields on the result of applyUpdate (BlueprintApplyResult, empty for an install) |
| SDK | BlueprintUpdatePreviewDto and BlueprintUpdateResultDto: TenantOwnedSkipped, TenantOwnedStaysDeleted (BlueprintTenantOwnedEntityDto) |
| octo-cli | PreviewBlueprintUpdate and UpdateBlueprint log a "tenant-owned, skipped" and a "deleted by the tenant, stays deleted" table (key, type, rtId, - for a deleted entity); nothing is printed for empty lists |
1 tenant-owned seed entity(ies) would be left untouched (tenant-owned, skipped):
Key Type Entity (rtId)
OwnedKept Test/Rule 65a1b2c3d4e5f60718293a4b
1 tenant-owned seed entity(ies) deleted by the tenant would stay deleted:
Key Type Entity (rtId)
OwnedGone Test/Rule -
The install path reports tenant-owned handling in its operation messages only, without these lists.
What a blueprint author does to change a tenant-owned value
A seed cannot change a value the tenant owns, and cannot clear a filled one. That is the intended behaviour. If a new version really has to change such a value on existing tenants, choose one of these:
- A CK migration step of the model (
Updateaction on the entities, see CK Model Migrations) or a blueprint migration script (UpdateBlueprint -m Migration, see Blueprints). The change is explicit, versioned and reviewable. - A documented manual step for the operator, with the exact value to set, when the change depends on the environment.
- The explicit blanking confirmation (
-cb), only when the intent is to clear a value; it cannot write a new one. - Correct the ownership if the attribute was classified wrong: an attribute a tenant types in is
TenantOwnedorSecret, an attribute the product must be able to correct isSeedOwned. A change of ownership is a minor bump of the CK model and takes effect for a tenant when it imports the new model version.
Do not work around the rules by shipping the value in a second seed entity or by relying on a forced re-apply: a forced re-apply follows exactly the same rules as an update.
Protect blueprint-locked entities from users
rtBlueprintLocked alone only steers updates; a user can still edit a locked entity. A blueprint can make the entities it owns read-only for users by shipping a System.Identity/DataPolicy for the type with ProtectBlueprintLocked: true. The behaviour, the error BLUEPRINT_LOCKED (message 6384) and the paths it covers are described in Blueprints. What a blueprint author must observe:
- Floor. Raise the blueprint's CK dependency to
System.Identity-[2.24.0,3.0). An older System.Identity silently drops the attribute. - The flag is
SeedOwned. Every update or forced re-apply puts the shipped value back; users cannot switch the protection off for good, and a later version can tighten or loosen it. - Ship an explicit
falseto take a protection back. A seed that merely omits the attribute keeps a storedtrue, because the blanking guard keeps the filled value. - One policy per protected type, in a policy group of its own. The policy is a restriction, not a grant: give the roles that work with the type their
PolicyPermission. - Entities the tenant should own stay editable with
rtBlueprintLocked: falsein the seed.
Availability
The protections arrived step by step. The first platform release that contains each of them:
| Protection | Platform release | Needs |
|---|---|---|
Attribute ownership TenantOwned, RuntimeState, Secret | earlier releases | CK model of the attribute carries the marker |
Adapter configuration is Secret | 3.5.7 | System.Communication 4.7.0 imported in the tenant |
DataPolicy mode and scope, Client, ApiResource switches are TenantOwned | 3.5.7 | System.Identity 2.23.0 imported in the tenant |
| Blanking guard | 3.5.3 | |
Update preview, blankedAttributes | 3.5.5 | service and octo-cli |
-fb, -cb, -ab | 3.5.5 | octo-cli |
Tenant-owned seed entities (rtBlueprintLocked: false) | 3.5.8 | |
tenantOwnedSkipped / tenantOwnedStaysDeleted in preview and result (REST, GraphQL, SDK, octo-cli) | the release after 3.5.9 that contains AB#6454 | service, octo-sdk and octo-cli |
ImportRt honours the blueprint lock | 3.5.9 | ProtectBlueprintLocked policy |
ProtectBlueprintLocked | with System.Identity 2.24.0 | the blueprint's floor |
An ownership marker acts on a tenant when the tenant has imported the CK model version that carries it; the service imports its embedded System models when a tenant is resolved. Check the model version of a tenant before you rely on a marker.
See also
- Blueprints — install, update, rollback, conflicts, migrations
- Seed ownership audit and its full table — which attributes every blueprint writes, who owns them, and the open findings
- Secret Attributes — the
Secretvalue type