Skip to main content

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.

LayerQuestionDecided by
Seed entity ownershipDoes the tenant own the whole entity?rtBlueprintLocked in the seed
Attribute ownershipDoes the tenant own this attribute?ownership in the CK model
Blanking guardWould the seed clear a value that is filled?The engine, for every SeedOwned attribute
Preview and confirmationWhat 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.)

ownershipWho owns the valueFresh installBlueprint update or forced re-applyExportRt
SeedOwned (default)The product. The blueprint author ships it and a new version may correct it.Seed value landsSeed value replaces the stored value, except that the blanking guard keeps a filled value when the seed is emptyExported
TenantOwnedThe tenant. An administrator or user sets it in the product.Seed value initialises itStored value is kept; the seed only initialises a new entityExported
RuntimeStateA service, an operator or a pipeline writes it at runtime.Seed value initialises itStored value is keptLeft out
SecretCredentials. Always the ownership of value type Secret.Seed value initialises it (a Secret value type is empty in a seed)Stored value is keptNever 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.

The other direction

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) are RuntimeState: the operator or the CD pipeline sets them.
  • The redirect and CORS URIs of an identity Client are RuntimeState: 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 saysOwnerFirst installUpdate, tenant holds itUpdate, tenant lacks it
flag absent or true (default)productcreatedrenewed from the seed (Merge, Full)created again (a deleted entity comes back)
rtBlueprintLocked: falsetenantcreated with the seed valuesnot 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 valueSeedResult
none, or emptyanythingseed value lands
filleddifferent, filled valueseed wins (a blueprint-owned correction, as before)
filledempty value (null, "", empty list, record of empty members)stored value kept, reported
filledattribute omitted, no CK defaultstored value kept, reported
filled and different from the CK defaultattribute omitted, attribute has a CK defaultstored value kept, reported as ResetToDefault
JSON text with a filled stringJSON text with an empty string at the same placestored 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
ShortLongOnMeaning
-fb--failOnBlankingPreviewBlueprintUpdateExit with code -5 when the update would blank at least one tenant value
-cb--confirm-blankingUpdateBlueprintConfirm one attribute, <rtId>:<attribute>; repeat the option for several. Everything not confirmed is kept
-ab--allow-blankingUpdateBlueprintConfirm 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.

ListMeaningentityId
tenantOwnedSkippedThe tenant still holds the entity; the update left it untouchedRuntime id on the tenant
tenantOwnedStaysDeletedThe previous seed contained it, the tenant deleted it, the update does not bring it backnull

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.

SurfaceWhere
RESTPOST {tenantId}/v1/blueprints/updates/preview (tenantOwnedSkipped, tenantOwnedStaysDeleted in the JSON body) and POST {tenantId}/v1/blueprints/updates/apply (same members in the result)
GraphQLblueprints.previewUpdate { tenantOwnedSkipped { key ckTypeId entityId wellKnownName } tenantOwnedStaysDeleted { ... } } and the same fields on the result of applyUpdate (BlueprintApplyResult, empty for an install)
SDKBlueprintUpdatePreviewDto and BlueprintUpdateResultDto: TenantOwnedSkipped, TenantOwnedStaysDeleted (BlueprintTenantOwnedEntityDto)
octo-cliPreviewBlueprintUpdate 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:

  1. A CK migration step of the model (Update action on the entities, see CK Model Migrations) or a blueprint migration script (UpdateBlueprint -m Migration, see Blueprints). The change is explicit, versioned and reviewable.
  2. A documented manual step for the operator, with the exact value to set, when the change depends on the environment.
  3. The explicit blanking confirmation (-cb), only when the intent is to clear a value; it cannot write a new one.
  4. Correct the ownership if the attribute was classified wrong: an attribute a tenant types in is TenantOwned or Secret, an attribute the product must be able to correct is SeedOwned. 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 false to take a protection back. A seed that merely omits the attribute keeps a stored true, 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: false in the seed.

Availability​

The protections arrived step by step. The first platform release that contains each of them:

ProtectionPlatform releaseNeeds
Attribute ownership TenantOwned, RuntimeState, Secretearlier releasesCK model of the attribute carries the marker
Adapter configuration is Secret3.5.7System.Communication 4.7.0 imported in the tenant
DataPolicy mode and scope, Client, ApiResource switches are TenantOwned3.5.7System.Identity 2.23.0 imported in the tenant
Blanking guard3.5.3
Update preview, blankedAttributes3.5.5service and octo-cli
-fb, -cb, -ab3.5.5octo-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#6454service, octo-sdk and octo-cli
ImportRt honours the blueprint lock3.5.9ProtectBlueprintLocked policy
ProtectBlueprintLockedwith System.Identity 2.24.0the 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​