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.1but the new model is3.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"
| Field | Required | Description |
|---|---|---|
ckModelId | Yes | Model ID with target version (e.g., MyModel-2.1.0) |
migrations | Yes | Array of migration step references |
migrations[].fromVersion | Yes | Source version this step migrates from |
migrations[].toVersion | Yes | Target version this step migrates to |
migrations[].scriptPath | Yes | Relative path to the migration script file |
migrations[].description | No | Human-readable description |
migrations[].breaking | No | Whether this step contains breaking changes (default: false) |
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.
| Type | Description | Parameters |
|---|---|---|
EntityExists | Entities matching target must exist | target |
EntityNotExists | No entities matching target may exist | target |
CkModelVersionInstalled | A specific CK model version must be installed | ckModelId, version |
AttributeEquals | An attribute must have a specific value | target, attribute, value |
Actions
Each migration step performs one of these actions:
| Action | Description |
|---|---|
Transform | Transform existing entities (change type, rename attributes, set values) |
Update | Update attribute values on existing entities |
Delete | Delete entities matching the target |
Add | Add new entities |
Transform Types
Used with action: Transform:
| Type | Description | Parameters |
|---|---|---|
ChangeCkType | Change the CK type of matching entities | newCkTypeId |
SetValue | Set a static value on an attribute | targetAttribute, value |
RenameAttribute | Rename an attribute | sourceAttribute, targetAttribute |
CopyAttribute | Copy an attribute value to a new attribute | sourceAttribute, targetAttribute |
DeleteAttribute | Remove an attribute from entities | targetAttribute |
MapValue | Map attribute values using a lookup table | targetAttribute, valueMapping |
Target Specification
The target block selects which entities a step operates on:
| Field | Description |
|---|---|
ckTypeId | CK type ID to target (e.g., MyModel/Widget) |
rtId | Specific runtime entity ID |
rtWellKnownName | Well-known name of the entity |
filter | Filter expression for fine-grained selection |
blueprintSourceOnly | If 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:
| Behavior | Description |
|---|---|
Fail | Stop the step on conflict (default) |
Skip | Skip the conflicting entity and continue |
Overwrite | Overwrite with new values |
Post-Validations
Validations run after migration steps complete:
| Type | Description | Parameters |
|---|---|---|
EntityExists | Verify entities of a type exist | target |
NoEntitiesOfType | Verify no entities of old type remain | target |
EntityCount | Verify entity count matches expected | target, 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:
- Scans
ConstructionKit/migrations/for*.yamlfiles - Embeds them as resources with the naming pattern
{RootNamespace}.migrations.{filename}.yaml - The
EmbeddedCkMigrationContentProviderloads 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:
| Option | Default | Description |
|---|---|---|
DryRun | false | Simulate migration without making changes |
CreateBackup | false | Create a tenant backup before migrating |
ContinueOnError | false | Continue with next step if a step fails |
Best Practices
- Version your migrations carefully — each migration step should be idempotent or use
preConditionsto check whether it needs to run. - Use
continueOnError: truefor steps that may partially apply (e.g., merging multiple subtypes where some may not exist in every tenant). - Add post-validations with
severity: Warningto verify migration results without blocking the process. - Test migrations locally before deploying to production tenants. Use
DryRun: trueto 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
fromVersionshould appear at most once in the chain.