Skip to main content

Blueprints

Blueprints are versioned, declarative bundles of Construction Kit (CK) models and runtime seed data that bootstrap a tenant — and continue to manage it across its lifetime. A blueprint can be installed, re-applied, updated, rolled back, uninstalled, and may depend on other blueprints. Versioned migration scripts transform tenant data when a blueprint's own version moves forward.

What a Blueprint Is​

A blueprint is essentially three things rolled into one artifact:

  • CK model dependencies — the schema (types, attributes, associations) the tenant needs before the seed data can be imported. The engine resolves and imports those CK models on apply.
  • Seed data — a runtime-model YAML file with the entities that should exist in the tenant after the apply.
  • Migration scripts — optional, versioned transformations executed when the blueprint moves from an older version to a newer one in an already-installed tenant.

Where a CK model defines the shape of data, a blueprint puts the first real data into a tenant. The two work in tandem: CK models alone leave a tenant empty; blueprints turn an empty tenant into a working one.

Properties​

PropertyDescription
VersionedSemVer (MyBlueprint-1.2.3). Version ranges express compatibility, like CK models.
Dependency-awareA blueprint may depend on other blueprints, resolved transitively at install time.
Owner-trackedEvery seed entity is tagged with rtBlueprintSource and rtBlueprintLocked.
UpdatableTenants are moved to newer versions via Safe / Merge / Full / Migration modes.
Rollback-ableDestructive operations create a tenant backup; Rollback restores the snapshot.
Multi-installA tenant can host several blueprints concurrently. Refcounted, cascade-uninstall optional.

Tenant-Scoped Storage​

Blueprint registry rows (BlueprintInstallation, BlueprintHistory, BlueprintBackup) live as CK entities inside the tenant's own runtime repository, alongside the seed data they describe. There is no cross-tenant collection — an apply in tenant X never writes outside tenant X. mongodump --db=<tenant> captures the registry along with the entities.

Blueprint Structure​

A blueprint is a directory containing a blueprint.yaml, an optional seed-data file, and optional migration scripts:

MyBlueprint-1.0.0/
├── blueprint.yaml
├── seed-data/
│ └── entities.yaml
└── migrations/
└── from-1.0.0.yaml

Blueprint YAML Schema​

$schema: https://schemas.meshmakers.cloud/blueprint-meta.schema.json
blueprintId: InfrastructureStarter-1.0.0
description: Infrastructure management starter blueprint

# CK models loaded into the tenant when this blueprint is applied
ckModelDependencies:
- System-[2.0,3.0)
- Commerce-[1.0,2.0)

# Other blueprints required before this one (resolved transitively, topo-sorted)
blueprintDependencies:
- BaseEntities-[1.0,)
- SecurityModel-[2.0,)

# Optional path to seed data (relative to blueprint root)
seedDataPath: seed-data/entities.yaml

# ... or a seed split across several files, merged into one model before import
seedDataPaths:
- seed-data/configurations/base.yaml
- seed-data/data-flows/camt053.yaml
- seed-data/identity/roles.yaml

# Optional migrations from older versions of this blueprint
migrations:
- fromVersion: "0.9.0"
scriptPath: "migrations/from-0.9.0.yaml"
FieldTypeDescription
$schemastringSchema URI for validation
blueprintIdstringUnique ID with version (Name-Major.Minor.Patch)
descriptionstringOptional description
ckModelDependenciesstring[]CK models with version ranges (auto-imported on apply)
blueprintDependenciesstring[]Other blueprints with version ranges (resolved transitively)
seedDataPathstringOptional path to seed-data file (runtime-model format)
seedDataPathsstring[]Optional list of seed-data files, merged into one model before import
migrationsarrayOptional list of migration scripts keyed by source version

Version Ranges​

Both ckModelDependencies and blueprintDependencies use the same range syntax as CK models:

FormatMeaning
1.0.0Exact version
[1.0.0,)Version 1.0.0 or higher
[1.0.0,2.0.0)Version >= 1.0.0 and < 2.0.0
(1.0.0,2.0.0]Version > 1.0.0 and <= 2.0.0
[1.5.0]Exactly version 1.5.0
CK ranges install the floor

For ckModelDependencies, the blueprint engine installs the range's lower bound on a fresh tenant (a tenant that already holds the same model at or above the floor is left untouched — no downgrade). The floor version must therefore be installable together with all sibling dependencies; after a dependency bump, raise the floor to the first version compiled against it. The seed-data file declares its own dependencies: pins independently of the manifest — keep both in sync. See Versioning Rules for the full picture.

Seed Data Format​

Seed data is a runtime-model YAML file. The blueprint engine stamps the source attributes during import; you do not write them yourself.

$schema: https://schemas.meshmakers.cloud/runtime-model.schema.json
dependencies:
- System-2.0.0
entities:
- rtId: 507f1f77bcf86cd799439011
ckTypeId: System/Entity
rtWellKnownName: InitialEntity
attributes:
- id: System/Name
value: My Initial Entity
- id: System/Description
value: Created by blueprint

Seed data is applied with upsert strategy: existing entities (matched by rtId) are updated; new ones are inserted.

Splitting the Seed Across Several Files​

One entities.yaml becomes unreadable long before a rich blueprint is finished — data flows, pipelines, configurations, identity roles and master data all end up interleaved in a file where no diff is reviewable and no feature has an owner. seedDataPaths replaces the single seedDataPath with a list, so the seed can be organised by feature:

MyBlueprint/
├── blueprint.yaml
└── seed-data/
├── configurations/base.yaml
├── data-flows/camt053.yaml # one pipeline + its data flow per file
├── identity/roles.yaml
└── master-data/accounts.yaml
seedDataPaths:
- seed-data/configurations/base.yaml
- seed-data/identity/roles.yaml
- seed-data/data-flows/camt053.yaml
- seed-data/master-data/accounts.yaml

Each file is a complete runtime-model document with its own $schema and dependencies. The engine loads all of them, unions their dependencies, concatenates their entities and imports the result as one model:

  • Order does not matter. The merged import writes every entity before any association, so an association in the first file may reference an entity declared in the last one.
  • Duplicate entities across files are rejected — identity is the CK type plus the rtId, so the same id under two types stays legal (octo-bpm validate warns about it) while a real repeat is refused.
  • A missing file is an error as soon as more than one file is declared: importing the rest would leave the tenant partially seeded and still report success.
  • octo-bpm validate warns about YAML files under seed-data/ that no path references.
  • Backwards compatible — seedDataPath keeps working unchanged.

There is no directory or glob form: blueprints are served to the runtime over GitHub Pages, which offers no directory listing, so the set of files is written down in the manifest that is fetched anyway.

Lifecycle​

Apply Flow​

ApplyBlueprintAsync(tenantId, blueprintId, force)
│
├── 1. Resolve transitive blueprint dependency closure (topo-sorted)
│
├── 2. Conflict-check (CK versions, entity ownership, rtId collisions)
│ → BlueprintApplicationResult.Conflicts; abort on hard conflicts
│
├── 3. For each blueprint in topo order:
│ ├── Idempotency: same version installed → no-op
│ │ same version installed + --force → re-apply (upsert)
│ │ different version installed → Update path
│ ├── Import CK model dependencies (auto-resolve via ICkModelUpgradeService)
│ ├── Apply seed data via IImportRtModelCommand (Upsert)
│ ├── Tag entities with rtBlueprintSource / rtBlueprintLocked / rtBlueprintAppliedAt
│ ├── Persist BlueprintInstallation
│ └── Publish BlueprintApplied event
│
└── 4. Append history entry, return result

Force Re-Apply and Runtime State​

Re-applying an installed blueprint version with --force (octo-cli -c InstallBlueprint -b MyBlueprint-1.0.0 -f) upserts every seed entity back to its seed values. This is the intended way to repair drifted seed data — but it also means any attribute value that operators or services changed after install is reset to what the seed ships, unless one of the following protects it:

  • Unlocked entities (rtBlueprintLocked = false) go through conflict resolution instead of being overwritten silently.
  • Runtime-state attributes are preserved. A CK attribute flagged isRuntimeState: true in its model declares that its value is owned by the running system, not by seed data: the runtime import keeps the tenant's current value on upsert and applies the seed value only when the entity is first inserted. The preserve logic sits in the shared runtime-model import, so it covers blueprint applies and plain ImportRt -r alike.

Well-known runtime-state attributes:

ModelAttributesSince
System.StreamDataArchive.Status — a force re-apply no longer deactivates activated archives1.7.0
System.CommunicationHostname, IngressEnabled, ChartVersion, ValuesYaml, Values on workloads — operator- or CD-set deployment values survive a force re-apply3.28.0
Operational state in seed data

When authoring a blueprint, avoid shipping mutable operational state (status flags, counters, deployment values) in seed entities. If the attribute is not flagged isRuntimeState in its CK model, every force re-apply resets it to the seed value — which can silently disable archives, undo deployment configuration, or reset processing watermarks on a production tenant. Either leave such attributes out of the seed, or flag them isRuntimeState in the owning CK model (a MINOR model change).

Secret Attributes in Seed Data​

A blueprint never ships credentials. For attributes of value type Secret, seed data may contain only no value, an empty value or null. Placeholders such as <set-after-install> or TODO_SET_CLIENT_SECRET are not allowed — they would be imported as an ordinary (encrypted) value.

entities:
- rtId: 65f1c0a2b3d4e5f601234567
ckTypeId: System.Communication/EMailReceiverConfiguration
rtWellKnownName: SupportMailbox
attributes:
- id: System.Communication/UserName
value: support@example.com
- id: System.Communication/Password
value: ""

The secret is imported as "not set". Because the ownership of a Secret attribute is always Secret, a re-apply — including a force re-apply — keeps a value that was set after installation. Set the secrets after installing the blueprint, in the Studio, with octo-cli or through the API (see Secret Attributes in the API).

The blueprint build lint fails when a seed sets any non-empty value for a Secret attribute, placeholders included; the error names the entity and the attribute, never the value. Secret sub-attributes inside records are checked too. If a real credential was ever committed, rotate it — removing it from the seed is not enough.

Entity Source Tracking​

Every seed entity is stamped with three system attributes when applied:

AttributeTypeDescription
rtBlueprintSourcestringOwning blueprint, full id (Infrastructure-1.0.0). Exactly one owner per entity.
rtBlueprintLockedbooltrue = managed by blueprint, updates will overwrite; false = user-released.
rtBlueprintAppliedAtDateTimeUTC timestamp of the most recent apply/update touching this entity.

A blueprint that ships an entity but wants to leave it user-editable from day one can set rtBlueprintLocked: false in its seed data.

Read-only for users: ProtectBlueprintLocked​

By default rtBlueprintLocked only steers blueprint updates: a user can still edit or delete a locked entity. A blueprint author can make the product-owned entities of a CK type read-only for users in the guarded entity-write paths (all GraphQL mutations), by shipping a data policy that opts the type in. This is done in the blueprint's seed data, not in code.

  1. Add a System.Identity/DataPolicy entity to the seed data, with the type in TargetCkTypeIds and the attribute ProtectBlueprintLocked: true (Boolean, default false). Use a policy of its own for each protected type; do not set the flag on shared policies such as a master-data policy.
  2. The policy is a restriction, not a grant: it applies whatever other policies grant, and derived types of the target type inherit it. Because the type is now covered by a policy, give the roles that may work with the type the normal grants (Read, Write, Delete) through the policy's permission, as for any other data policy.
  3. Raise the blueprint's dependency on System.Identity to the version that introduced ProtectBlueprintLocked.

Effect for non-system callers on a protected type (the paths that are not covered are listed at the end of this section):

  • Updating, replacing or deleting an entity whose stored rtBlueprintLocked is true is refused with the stable error BLUEPRINT_LOCKED (message number 6384). The whole change set is rejected, nothing is written.
  • Entities with rtBlueprintLocked false or absent stay fully editable. Seed the entities that the tenant should own with rtBlueprintLocked: false.
  • Users cannot set rtBlueprintLocked, rtBlueprintSource or rtBlueprintAppliedAt on insert and cannot change them on update; an unchanged round trip of a form passes. Other types are unchanged.
  • Reads are never affected.
  • The policy's enforcement mode applies: Enforce refuses, AuditOnly lets the change through and publishes one DataPermissions.BlueprintLockViolation audit event.

Blueprint install, update and forced re-apply run in the system context and are not blocked. Models/ImportRt applies the same rule end to end: the import job runs on behalf of the initiating caller and fails as a whole, before anything is written, when the file would overwrite a locked entity of a protected type (AB#6392). rtBlueprintLocked, rtBlueprintSource and rtBlueprintAppliedAt of an exported file are stripped from the incoming entities of protected types, so an export from another tenant can still be imported (the entities become tenant-owned). A pipeline node with Identity: ServiceAccount is guarded like a user; use Identity: System for pipelines that must write product-owned entities. The setting is read from a cache that is refreshed within a minute. Associations of locked entities are not covered.

Creating a Blueprint​

A blueprint is just three files (or two if it has no migrations). The minimum viable case:

Step 1 — Create the folder layout​

HelloBlueprint-1.0.0/
├── blueprint.yaml
└── seed-data/
└── entities.yaml

Step 2 — Write blueprint.yaml​

$schema: https://schemas.meshmakers.cloud/blueprint-meta.schema.json
blueprintId: HelloBlueprint-1.0.0
description: |
Seeds two AutoIncrement counters used by the demo invoice and order flow.
ckModelDependencies:
- System-[2.0,3.0)
seedDataPath: seed-data/entities.yaml

Step 3 — Write seed-data/entities.yaml​

$schema: https://schemas.meshmakers.cloud/runtime-model.schema.json
dependencies:
- System-2.0.0
entities:
- rtId: 65d5c447b420da3fb1238201
ckTypeId: System/AutoIncrement
rtWellKnownName: InvoiceCounter
attributes:
- id: System/AutoIncrement.End
value: 999999
- id: System/AutoIncrement.CurrentValue
value: 1000
- id: System/AutoIncrement.Format
value: "INV-{0:D6}"
- rtId: 65d5c447b420da3fb1238202
ckTypeId: System/AutoIncrement
rtWellKnownName: OrderCounter
attributes:
- id: System/AutoIncrement.End
value: 99999
- id: System/AutoIncrement.CurrentValue
value: 100
- id: System/AutoIncrement.Format
value: "ORD-{0:D5}"

Step 4 — Place the folder into a catalog​

The simplest catalog is the local file system. The asset-repo service expects blueprints at the path configured via LocalFileSystemBlueprintCatalogOptions.RootPath. For local development that defaults to ~/.octo/local-blueprint-catalog/:

~/.octo/local-blueprint-catalog/
└── blueprints/v1/
└── HelloBlueprint/
└── 1.0.0/
├── blueprint.yaml
└── seed-data/
└── entities.yaml

For published catalogs (GitHub-Pages-hosted), the same layout applies, but a generated catalog.json index sits at each level — see GitHub Catalog Layout below.

Step 5 — Install​

octo-cli -c InstallBlueprint -b HelloBlueprint-1.0.0

Or via Refinery Studio: see the Studio user guide.

Building On Top of Another Blueprint​

Blueprints compose through dependencies, not by bundling. To extend an existing blueprint:

  1. Declare the dependency in your blueprint.yaml:

    blueprintDependencies:
    - HelloBlueprint-[1.0,2.0)
  2. Reference its CK models indirectly. You don't list them again — the dependency closure already imports them.

  3. Add your own seed data. Your entities live in your blueprint's rtBlueprintSource namespace; the entities from HelloBlueprint remain owned by it.

  4. Apply normally. The engine resolves the transitive closure, applies dependencies first (topo-sorted), then your blueprint:

    octo-cli -c InstallBlueprint -b ExtendedHello-1.0.0

    ListBlueprintInstallations then shows both blueprints — HelloBlueprint carries IsDependency: true.

Ownership Rules​

  • Each entity has exactly one owning blueprint (rtBlueprintSource).
  • A dependent blueprint cannot mutate entities owned by its dependencies through seed data — that path raises a UserModified / OwnershipConflict during apply.
  • If the dependent really needs to evolve those entities, ship a migration script that targets them by rtId or by rtWellKnownName + blueprintSourceOnly: true.

Uninstall and Refcounts​

  • Uninstalling ExtendedHello does not remove HelloBlueprint if any other installed blueprint still depends on it (refcount > 0).
  • octo-cli -c UninstallBlueprint -n ExtendedHello -c cascades: removes ExtendedHello and orphan-cleans transitively-depending blueprints whose refcount drops to zero.

Update Modes​

ModeBehaviour
SafeAdd new entities only. Existing entities are left alone, even if locked.
MergeAdd new + upsert locked entities. Unlocked entities raise UserModified conflicts (default: skip).
FullLike Merge, plus delete entities that exist in the tenant but no longer in the seed. Unlocked → conflict.
MigrationExecute the migration script from the installed version to the target. Required for any non-additive change.

A pre-update backup is created by default (CreateBackup = true). Disable it with CreateBackup = false only when you have an alternate snapshot mechanism.

Conflict Resolution​

A conflict is raised when an unlocked entity (rtBlueprintLocked = false) is in the way of an update.

TypeTriggered when
UserModifiedThe seed wants to update this entity, but the tenant entity has been unlocked.
DeleteModifiedFull mode wants to delete this entity (no longer in seed), but the tenant entity has been unlocked.

Default per-entity resolution is Skip. The caller can override per-entity:

ResolutionBehaviour
KeepUserKeep the user's version, skip the blueprint change.
KeepBlueprintApply the blueprint's version. UserModified: seed is re-applied and the entity is re-locked. DeleteModified (Full only): entity is erased.
MergeCurrently treated as KeepUser (semantic 3-way merge is out of scope).
SkipSkip this entity.

Migration Scripts​

For non-additive changes (rename, delete, transform), ship a migration script and reference it from blueprint.yaml:

# MyBlueprint-2.0.0/migrations/from-1.0.0.yaml
$schema: https://schemas.meshmakers.cloud/blueprint-migration.schema.json
sourceVersion: "1.0.0"
targetVersion: "2.0.0"
description: "Migration from v1 to v2"

preConditions:
- type: EntityExists
target:
ckTypeId: System/Entity
rtWellKnownName: MainConfig

steps:
- stepId: rename-config-field
action: Transform
target:
ckTypeId: System/Entity
blueprintSourceOnly: true
transform:
type: Rename
sourceAttribute: LegacyVersion
targetAttribute: Version

- stepId: delete-deprecated
action: Delete
target:
ckTypeId: System/Entity
rtWellKnownName: LegacyConfig
blueprintSourceOnly: true

postValidations:
- validationId: still-have-config
type: EntityCount
target:
ckTypeId: System/Entity
expectedCount: 5
severity: Error

Reference from the manifest:

migrations:
- fromVersion: "1.0.0"
scriptPath: "migrations/from-1.0.0.yaml"

Supported step actions​

ActionPurpose
AddInsert an entity (data carries the full RtEntityTcDto payload).
UpdateUpdate attributes on matching entities (data is a { attributeName: value } dict).
DeleteErase matching entities (DeleteOptions.Erase — permanent).
RenameRename an attribute on matching entities (shorthand for Transform of type Rename).
TransformType-driven: Rename, Copy, Delete, SetValue, MapValue.

For CK model migrations (when the schema itself evolves), see CK Model Migrations. Blueprint migrations transform runtime entities; CK migrations transform the schema and the entities together.

Multi-Blueprint Installation​

A tenant can host any number of blueprints concurrently. Two services track this state:

InterfacePurpose
ITenantBlueprintInstallationsThe current set of installed blueprints (one row per blueprint, with IsDependency flag).
ITenantBlueprintHistoryAppend-only operation log (install, update, rollback, uninstall) with timestamps and counts.

Both are persisted as CK entities (System/BlueprintInstallation, System/BlueprintHistory) inside the tenant's own runtime repository.

Backup and Rollback​

Every update creates a pre-update backup by default. Rollback restores the entire tenant snapshot — it is a full restore, not a semantic undo of migration steps. After rollback, the BlueprintInstallation rows match the snapshot; partial undo is not supported.

Catalogs​

CatalogDescription
LocalFileSystemBlueprintCatalogLoads blueprints from the file system.
EmbeddedResourceBlueprintCatalogLoads blueprints from assembly resources.
PublicGitHubBlueprintCatalogReads blueprints from a public GitHub Pages site.
PrivateGitHubBlueprintCatalogReads blueprints from a private/internal GitHub repository (writes via Octokit).

GitHub Catalog Layout​

blueprints/v1/
├── catalog.json # Root catalog index
└── m/ # First letter of blueprint name (lowercase)
└── MyBlueprint/
├── catalog.json # Library catalog (one entry per major version)
└── 1/
├── catalog.json # Version catalog (list of versions)
└── MyBlueprint-1.0.0/
├── blueprint.yaml
├── seed-data/
└── migrations/

The three catalog.json levels are generated by the publish flow on the engine side — application code talks to IBlueprintCatalogManager, not to the catalog files directly.

Best Practices​

  1. Small, focused blueprints. One blueprint per domain/feature. Compose via blueprintDependencies, not by bundling unrelated entities.
  2. Use version ranges for dependencies. [1.0,) keeps things flexible; pinning exact versions is fine for blueprintId but rarely useful in dependencies.
  3. Sparse seed data. Ship essential bootstrap data only — no test data, no per-customer specifics, and never credentials (see Secret attributes in seed data).
  4. Lock managed entities. Default rtBlueprintLocked is true; only override to false when you intend the user to take ownership immediately. To keep users from editing locked entities, opt the type in with ProtectBlueprintLocked.
  5. Migration scripts for breaking changes. Schema renames, deletes, and value transformations need an explicit script. Additive changes work via Merge alone.
  6. Test with dry-run. UpdateBlueprint -dr simulates without persisting.
  7. Keep backups. Default backup creation is on; only disable it when you have an alternate snapshot mechanism.

See Also​