Skip to main content

Construction Kit Model Migrations

When a Construction Kit (CK) model evolves to a new version, existing runtime entities in tenant databases may need to be updated to match the new schema. CK model migrations automate this process by defining versioned transformation scripts that update entity data (rename types, change attributes, etc.) during model import.

Overview

CK model migrations consist of two parts:

  • Migration Metadata (migration-meta.yaml) — declares available migration steps and their version chain
  • Migration Scripts (*.yaml) — define the actual data transformations for each version step

Migration files are placed in the ConstructionKit/migrations/ folder of a CK model project and are automatically embedded as assembly resources at build time.

How Migration Path Resolution Works

When a tenant receives a new CK model version, the engine resolves the migration path using a multi-layer strategy:

Auto-Bridging Version Gaps

The engine automatically bridges version gaps at both ends of the migration chain without requiring developers to create empty migration entries:

  • Start gap: When the tenant is at an older version (e.g., 2.2.0) than the earliest migration entry point (e.g., 3.0.1), the engine creates a no-op bridge step. No data migration is needed because the tenant's data is compatible with the start of the chain.
  • End gap: When the migration chain doesn't reach the exact target version (e.g., chain ends at 3.1.1 but the new model is 3.1.2), the engine executes all available migrations and treats the remaining version bump as schema-only.

Example: A tenant at version 2.2.0 receiving model version 3.1.2 with migrations defined from 3.0.1:

2.2.0 → 3.0.1  (auto-bridge, no-op)
3.0.1 → 3.0.2 (migration script executed)
3.0.2 → 3.0.3 (migration script executed)
3.0.3 → 3.1.0 (migration script executed)
3.1.0 → 3.1.1 (migration script executed)
3.1.1 → 3.1.2 (auto-bridge, schema-only)

Creating Migrations

File Structure

Place migration files in the ConstructionKit/migrations/ directory of your CK model project:

MyModel/
└── ConstructionKit/
├── ckModel.yaml
├── types/
├── attributes/
└── migrations/
├── migration-meta.yaml
├── 1.0.0-to-2.0.0.yaml
└── 2.0.0-to-2.1.0.yaml

Migration Metadata

The migration-meta.yaml file declares the available migration chain:

$schema: https://schemas.meshmakers.cloud/ck-migration-meta.schema.json
ckModelId: MyModel-2.1.0
migrations:
- fromVersion: "1.0.0"
toVersion: "2.0.0"
scriptPath: 1.0.0-to-2.0.0.yaml
description: "Rename Widget type to Component"
breaking: true
- fromVersion: "2.0.0"
toVersion: "2.1.0"
scriptPath: 2.0.0-to-2.1.0.yaml
description: "Add default status attribute to all Components"
FieldRequiredDescription
ckModelIdYesModel ID with target version (e.g., MyModel-2.1.0)
migrationsYesArray of migration step references
migrations[].fromVersionYesSource version this step migrates from
migrations[].toVersionYesTarget version this step migrates to
migrations[].scriptPathYesRelative path to the migration script file
migrations[].descriptionNoHuman-readable description
migrations[].breakingNoWhether this step contains breaking changes (default: false)
info

You only need to define migration entries for steps that require data transformations. The engine auto-bridges any version gaps where no data migration is needed.

Migration Scripts

Each migration script defines preconditions, transformation steps, and post-validations:

$schema: https://schemas.meshmakers.cloud/ck-migration.schema.json
sourceVersion: "1.0.0"
targetVersion: "2.0.0"
description: |
Renames the Widget CK type to Component.

preConditions:
- type: EntityExists
target:
ckTypeId: MyModel/Widget

steps:
- stepId: rename-widget-to-component
description: "Change CkTypeId from Widget to Component"
action: Transform
target:
ckTypeId: MyModel/Widget
transform:
type: ChangeCkType
newCkTypeId: MyModel/Component
onConflict: Skip
continueOnError: false

postValidations:
- validationId: no-widgets-remain
description: "Ensure no entities with legacy Widget type exist"
type: NoEntitiesOfType
target:
ckTypeId: MyModel/Widget
severity: Warning

Migration Script Reference

Pre-Conditions

Conditions that must be met before the migration runs. If a precondition is not met, the migration step is skipped.

TypeDescriptionParameters
EntityExistsEntities matching target must existtarget
EntityNotExistsNo entities matching target may existtarget
CkModelVersionInstalledA specific CK model version must be installedckModelId, version
AttributeEqualsAn attribute must have a specific valuetarget, attribute, value

Actions

Each migration step performs one of these actions:

ActionDescription
TransformTransform existing entities (change type, rename attributes, set values)
UpdateUpdate attribute values on existing entities
DeleteDelete entities matching the target
AddAdd new entities

Transform Types

Used with action: Transform:

TypeDescriptionParameters
ChangeCkTypeChange the CK type of matching entitiesnewCkTypeId
SetValueSet a static value on an attributetargetAttribute, value
RenameAttributeRename an attributesourceAttribute, targetAttribute
CopyAttributeCopy an attribute value to a new attributesourceAttribute, targetAttribute
DeleteAttributeRemove an attribute from entitiestargetAttribute
MapValueMap attribute values using a lookup tabletargetAttribute, valueMapping

Target Specification

The target block selects which entities a step operates on:

FieldDescription
ckTypeIdCK type ID to target (e.g., MyModel/Widget)
rtIdSpecific runtime entity ID
rtWellKnownNameWell-known name of the entity
filterFilter expression for fine-grained selection
blueprintSourceOnlyIf true, only target blueprint-created entities (default: false)

Filter Expressions

Filters allow fine-grained entity selection within a target:

filter:
attribute: Status
operator: Eq
value: "active"

Supported operators: Eq, Ne, Exists, NotExists, Contains, StartsWith

Filters can be combined with boolean logic:

filter:
and:
- attribute: Status
operator: Eq
value: "active"
- attribute: Type
operator: Ne
value: "deprecated"

Conflict Behavior

Controls what happens when a migration step encounters a conflict:

BehaviorDescription
FailStop the step on conflict (default)
SkipSkip the conflicting entity and continue
OverwriteOverwrite with new values

Post-Validations

Validations run after migration steps complete:

TypeDescriptionParameters
EntityExistsVerify entities of a type existtarget
NoEntitiesOfTypeVerify no entities of old type remaintarget
EntityCountVerify entity count matches expectedtarget, expectedCount

Each validation has a severity (Error or Warning). Warnings are logged but don't fail the migration.

Complete Example

This example shows a migration that renames a CK type and merges two subtypes into a single type:

migration-meta.yaml:

$schema: https://schemas.meshmakers.cloud/ck-migration-meta.schema.json
ckModelId: System.Communication-3.1.1
migrations:
- fromVersion: "3.0.1"
toVersion: "3.0.2"
scriptPath: 3.0.1-to-3.0.2.yaml
description: "Rename DataPipeline to DataFlow"
breaking: true
- fromVersion: "3.0.2"
toVersion: "3.0.3"
scriptPath: 3.0.2-to-3.0.3.yaml
description: "Retry rename for tenants where previous migration failed"
breaking: true
- fromVersion: "3.0.3"
toVersion: "3.1.0"
scriptPath: 3.0.3-to-3.1.0.yaml
description: "Merge EdgeAdapter/MeshAdapter into Adapter"
breaking: true
- fromVersion: "3.1.0"
toVersion: "3.1.1"
scriptPath: 3.1.0-to-3.1.1.yaml
description: "Retry merge after engine fix"
breaking: true

3.0.3-to-3.1.0.yaml (type hierarchy unification):

$schema: https://schemas.meshmakers.cloud/ck-migration.schema.json
sourceVersion: "3.0.3"
targetVersion: "3.1.0"
description: |
Merges EdgeAdapter/MeshAdapter into Adapter and
EdgePipeline/MeshPipeline into Pipeline.

steps:
- stepId: migrate-edge-adapter
description: "Migrate EdgeAdapter to Adapter"
action: Transform
target:
ckTypeId: System.Communication/EdgeAdapter
blueprintSourceOnly: false
transform:
type: ChangeCkType
newCkTypeId: System.Communication/Adapter
onConflict: Skip
continueOnError: true

- stepId: migrate-mesh-adapter
description: "Migrate MeshAdapter to Adapter"
action: Transform
target:
ckTypeId: System.Communication/MeshAdapter
blueprintSourceOnly: false
transform:
type: ChangeCkType
newCkTypeId: System.Communication/Adapter
onConflict: Skip
continueOnError: true

- stepId: migrate-edge-pipeline
description: "Migrate EdgePipeline to Pipeline"
action: Transform
target:
ckTypeId: System.Communication/EdgePipeline
blueprintSourceOnly: false
transform:
type: ChangeCkType
newCkTypeId: System.Communication/Pipeline
onConflict: Skip
continueOnError: true

Build Integration

Migration files are automatically embedded as assembly resources during build. This is controlled by the MSBuild property OctoEmbedCkMigrations (default: true).

The build system:

  1. Scans ConstructionKit/migrations/ for *.yaml files
  2. Embeds them as resources with the naming pattern {RootNamespace}.migrations.{filename}.yaml
  3. The EmbeddedCkMigrationContentProvider loads them at runtime

To disable embedding (e.g., for file-system-based migrations):

<PropertyGroup>
<OctoEmbedCkMigrations>false</OctoEmbedCkMigrations>
</PropertyGroup>

Migration Options

When executing migrations programmatically, these options are available:

OptionDefaultDescription
DryRunfalseSimulate migration without making changes
CreateBackupfalseCreate a tenant backup before migrating
ContinueOnErrorfalseContinue with next step if a step fails

Best Practices

  • Version your migrations carefully — each migration step should be idempotent or use preConditions to check whether it needs to run.
  • Use continueOnError: true for steps that may partially apply (e.g., merging multiple subtypes where some may not exist in every tenant).
  • Add post-validations with severity: Warning to verify migration results without blocking the process.
  • Test migrations locally before deploying to production tenants. Use DryRun: true to validate without changing data.
  • No need for empty migration entries — the engine auto-bridges version gaps. Only create migration scripts for steps that actually transform data.
  • Keep migration chains linear — avoid branching migration paths. Each fromVersion should appear at most once in the chain.