Skip to content

Extending the Metamodel with Type Definitions

Tailor CrossModel's metamodel to your own standards

Out of the box, CrossModel knows about entities, attributes, relationships, identifiers and data models. Type definitions let you extend that metamodel with your own types - so you can attach extra properties to model objects, capture house standards, and reuse them across every model in your workspace.

A typical use case: your organisation wants every entity to record a data owner and a data classification, or you model in a fixed pattern such as Data Vault (Hubs, Links, Satellites) and want each pattern to carry its own default attributes and metadata. Type definitions make those rules first-class parts of the model instead of conventions you have to remember.

This tutorial covers:

  • The kinds of type definition CrossModel supports
  • Creating a custom property definition
  • Building a type hierarchy with extends
  • Using a type by assigning it to a model object
  • How CrossModel resolves and overrides inherited property values

What you can define

A type definition describes a kind of model object. CrossModel has six definition kinds - one for each core metamodel concept, plus a generic one for custom properties. Each has its own button on the New Definition toolbar:

Icon Definition kind Extends which object
Entity Definition icon Entity Definition Logical entities
Attribute Definition icon Attribute Definition Entity attributes
Relationship Definition icon Relationship Definition Relationships
Identifier Definition icon Identifier Definition Identifiers
DataModel Definition icon DataModel Definition Data models
Custom Property Definition icon Custom Property Definition Custom properties

Every definition you create implicitly builds on a built-in root for its domain (Entity, Attribute, Relationship, Identifier, DataModel, CustomProperty). You never have to declare that root - it is always there as the base of the chain.


Step 1 - Create a custom property definition

The most common starting point is a custom property definition: a reusable, typed property you can attach to object definitions.

In the Model Explorer, right-click your data model and choose New Definition → Custom Property Definition, or click the custom property definition button in the toolbar. CrossModel creates the file and opens it in the form editor with the cursor already in the Name field.

The New Definition submenu with all six definition kinds

Now fill in the form. Suppose BrightGreen wants a governance property that records who owns the data in an entity - give it a name, description and data type, and optionally mark it mandatory.

Create a Custom Property Definition

A custom property definition can carry the following metadata, which describes the shape and constraints of the value its instances will hold:

Field Purpose
Data type The value's data type (see below)
Length Maximum length
Precision Total digits
Scale Fractional digits
Unit The unit a Duration is counted in
Mandatory Whether instances must provide a value
Value A default value supplied to instances
Allowed Values A fixed list of values instances may choose from (see below)
Abstract Marks the definition as a building block only - not meant to be used directly

Supported data types are: Text, Binary, Integer, Decimal, Float, Duration, Date, Time, DateTime, Boolean, UUID, Geometry and Geography.

Which fields apply to which data type

Length, Precision, Scale and Unit only describe some data types, and the form knows which. Pick a data type and the fields that do not apply to it are greyed out rather than hidden, so you can see at a glance what that type can be constrained by:

Data type Length Precision Scale Unit
Text, Binary
Decimal
Float
Time, DateTime
Duration
Integer, Date, Boolean, UUID, Geometry, Geography

Unit is new in v1.11.0. It turns a bare number into a stated quantity: a retention period of 7 means nothing until the definition also says years. The units are milliseconds, seconds, minutes, hours, days, months and years, and the value itself stays a plain whole number, so nothing has to parse 7 years back apart.

Setting Unit on anything but a Duration is an error - "Unit is only applicable to Duration datatype." - which is mostly a concern when you edit the .cm file by hand, since the form disables the field for you. In the file it is written without quotes, and it belongs after scale and before mandatory:

customPropertyDefinition:
    id: RetentionPeriod
    name: "RetentionPeriod"
    datatype: "Duration"
    unit: years
    value: "7"

Values are checked against the data type

A value that cannot be the data type it claims to be is reported in the Problems panel, on the definition's own default value and on every instance that supplies one. As of v1.11.0 the checked types are Text (length only), Integer, Decimal, Float, Boolean, Date, Time, DateTime, UUID and Duration.

UUID and Duration are the two v1.11.0 added:

  • UUID wants the canonical 8-4-4-4-12 hexadecimal form. not-a-uuid gives "Value 'not-a-uuid' is not a valid UUID. Expected format: 8-4-4-4-12 hexadecimal digits.", while 3f2504e0-4f89-11d3-9a0c-0305e82c3301 passes.
  • Duration wants a whole number, because the unit is carried separately. 1.5 gives "Value '1.5' is not a valid Duration. Expected a whole number." To say an hour and a half, use 90 with unit: minutes.

Binary, Geometry and Geography are not format-checked: there is no single string convention for them to check against.

Changing the data type re-checks the value you already had

The value is validated against whichever data type the field currently says, not the one it had when you typed it. Switch a property from Duration to UUID and a perfectly good 3 immediately reports as an invalid UUID. That is the model telling you the truth, but it does surprise people who expect the error to be about the field they just touched.


Step 1b - Constrain a property to a list of allowed values

A free-text governance property is only as good as the discipline of the people filling it in. Owner, owner, Data Owner and dataowner are four different strings as far as any downstream consumer is concerned. Allowed values turn such a property into a controlled vocabulary.

Scroll to the Allowed Values section at the bottom of the custom property definition form and press Add Allowed Value once per permitted value. Type the value and press Enter - the row is committed and a new empty row opens, so you can enter the whole list without reaching for the mouse. For a Data Classification property, BrightGreen might allow exactly Public, Internal, Confidential and Restricted.

The effect is immediate: the definition's own Value field turns into a dropdown offering exactly those four values.

Allowed values on a custom property definition, and the resulting Value dropdown

Once the list is non-empty, the behaviour of every instance of that property changes:

  • The Value field turns into a dropdown restricted to the list - free text is only accepted while the list is empty.
  • The definition's own default Value is validated against the list too, so a typo in the default is caught at the source rather than on every object that inherits it.
  • A value outside the list produces the warning "Value '…' is not in the allowed values: …" in the Problems panel.

Allowed values are inherited, and the nearest list wins

Like everything else on a definition, the list travels down the extends chain - but it is replaced, not merged. CrossModel walks the chain and uses the first definition that declares a non-empty list. A child that declares its own allowedValues therefore fully overrides its parent's vocabulary; a child that declares none simply inherits the parent's.

Allowed values also reach your templates

Code generation sees both halves of a custom property. In a template, entity.customProperties.DataClassification renders the value, while entity.customProperties.$allowedValues.DataClassification gives you the full permitted list - useful for generating CHECK constraints or enum types straight from the model. See Code Generation.

Mandatory values are a warning, not an error

A missing value on a mandatory custom property is reported as a warning rather than an error. Modelling is iterative - an incomplete governance property should show up in the Problems panel without making the model invalid and blocking diagrams, generation, or a commit. The same applies to a value that falls outside the allowed list.


Step 2 - Build a type hierarchy with extends

Type definitions can inherit from one another through the Extends setting. A child definition inherits everything from its parent and may add to or override it. Each definition has a single parent, forming an inheritance chain.

Hierarchies shine when you model in a repeatable pattern. As an example, we define a governed entity to be something that should always have a data owner and a reference to an external enterprise data catalog. The we distinghuis between historical and nonhistorical entities as subtypes to classify whether or not data is needed historically.

Create the base entity definition first, mark it Abstract, and add its shared attributes:

Abstract base entity definition

Now also add custom properties, use the custom property type data owner created in the previous step:

Abstract base entity definition custom properties

You need to use the popup to set the custom property type:

Abstract base entity definition custom property details

Then create the concrete HistoricalEntity and set its Extends to GovernedEntity. It inherits the base attributes and custom property automatically, and you add only the attributes specific to a HistoricalEntity:

Child entity definition extending the base

Notice that the custom properties also show as inherited:

Child entity definition extending the base custom properties

We can also add a NonHistoricalEntity - this would also extend GovernedEntity and contributes its own attributes. The result is a clean hierarchy:

GovernedEntity (abstract)
├── HistoricalEntity
├── NonHistoricalEntity

You can go deeper than one level - a definition may extend another definition that itself extends a third, and so on. Resolution always walks the full chain.

Attributes and custom properties can be typed too

A type reference works the same way everywhere, not just on entities. An attribute can point at an Attribute Definition and a custom property at a Custom Property Definition - pulling in that definition's shape and defaults. This is exactly how you use a type, which is the next step.


Step 3 - Use a type by assigning it

A definition only does something once an object references it. Every object has a Type setting that points at the definition to apply:

Object References a
Logical entity Entity Definition
Attribute Attribute Definition
Relationship Relationship Definition
Identifier Identifier Definition
Data model DataModel Definition
Custom property Custom Property Definition

To type an entity, open it in the form editor and pick the type from the Type dropdown in its properties - for example, making the Customer entity in the DWH model a HistoricalEntity by selecting HistoricalEntity.

Assigning a type to an entity

Using type definitions

In order to use a type definition, it should be defined in the model where you want to use it, or the model should have a dependency to the model where the type definitions are defined.

That single setting pulls in everything HistoricalEntity resolves to - the attributes and custom properties from the abstract base, the HistoricalEntity's own attributes, and the hashing custom property - without you re-declaring any of it. In the form editor these inherited members appear alongside the entity's own, rendered in a muted style with an Inherited from … tooltip so you can always see where each value originates.

The entity below is typed Formal_entity. Its Type field carries that reference, and the first attribute in the grid - Record_source, shown in a muted italic - comes from the type definition rather than from the entity itself:

An entity typed by a definition, showing an inherited attribute

Inherited members in the form editor

The Type dropdown only offers compatible definitions

The Type dropdown is scoped to the matching kind of definition - an entity only lists Entity Definitions, an attribute only lists Attribute Definitions, and so on. Definitions marked Abstract are left out, since they are building blocks rather than types you assign directly.


Step 4 - Overriding inherited values

The point of inheritance is sensible defaults you can override where needed. CrossModel resolves the effective value of every field from three tiers, in priority order:

  1. The item's own value - set on the object itself, or inherited through its extends chain. The most specific (lowest) explicitly-set value wins.
  2. The type definition's value - supplied by the referenced type and that definition's own extends chain.
  3. The built-in default - used only when nothing above sets the field.

In short: the lowest explicitly-set value wins, and any field you don't set falls back to the nearest ancestor (or the type definition) that does.

Clearing an override re-inherits

An override you set stays local until you clear it - CrossModel won't silently drop it just because it happens to match a parent's value. The one gesture that removes an override is emptying the field in the form: clearing it drops the local value and the field re-inherits from the parent (or type) again. This gives you a predictable way to "reset to inherited".

Collections merge by name

Inherited and locally-added items live in the same collection. Items are matched across levels by their identifier: the same identifier means the child refines the inherited item, while a new identifier means a new item added at this level. Inherited items are listed first (deepest ancestor first), followed by the items you add locally.


Step 5 - Derive names and descriptions with property templates

Inheritance copies a value down. A property template goes further: it computes a value from the object the type is applied to.

The use case is house naming standards. If every data vault key attribute should be called " Key", you can either trust everyone to type it consistently, or you can state the rule once on the definition and let CrossModel apply it.

Open a definition in the form editor and find the Property Templates section. Each row has two fields:

Field Purpose
Property Which field to derive. Only name and description can be templated.
Template The text to produce, with {{...}} placeholders filled in from the object.

The Property Templates section on an entity definition

In the code editor the same thing looks like this:

attributeDefinition:
    id: DataVaultKey
    name: "Data Vault Key"
    propertyTemplates:
      - property: name
        template: "{{entity.name}} Key"
      - property: description
        template: "Key attribute for {{entity.name}}"

You then apply DataVaultKey as the Type of an attribute, and the template fills that attribute's name in from the entity around it.

An attribute's Type is in its row detail, not the grid

The Attributes grid on an entity shows Name, Datatype, Length, Precision, Scale, the identifier and mandatory flags, and Description - but no Type column. Open the row's detail with the external-link button beside it; Type is the third field, under General. It is the same popup route you used to set a custom property's type earlier.

What you can put in a template

A placeholder is a path, resolved against the object the template is being evaluated for. The context exposes entity, mapping, relationship, datamodel and self, depending on what kind of object you are on. A path segment may be $first to take the first element of a collection.

Templates inherit, and can be overridden

propertyTemplates merge along the extends chain like every other collection in this tutorial, keyed by the property being templated. A child definition that templates name replaces its parent's name template and leaves the parent's description template alone.

A derived value behaves like an inherited one

Give an attribute nothing but an id and this type, and it arrives named:

      - id: VaultKey
        type: dwh-definitions.DataVaultKey

The derived attribute name shown in the entity's Attributes grid

Customer Key appears in the grid in the same italics as any other inherited value, and it is not written into the file - the attribute there still holds only its id and its type. The value is recomputed, so renaming the entity renames everything derived from it.

Typing over it makes it yours, like any other override, and it is then stored on the object.

A template fills a field only when nothing else has

That is the whole rule, and it is what makes templates useful rather than surprising: they never overwrite a value you set.

It has a practical consequence for name templates. Name is validated as non-empty, so the form will not let you create a nameless attribute for the template to fill, and clearing an existing name reports "The name cannot be empty" instead of falling back to the template. Write the attribute in the Code Editor with only its id and type, as above, and the name is derived when you save.

An unresolvable path is reported, not ignored

If a placeholder cannot be resolved against the object the template is applied to, the instance gets a validation error naming the problem - "Property template for 'name' cannot be evaluated on this instance". A template that quietly produced a blank name would be much worse.

Two different things are called templates

Property templates derive a field value on the object - a name or a description. Code templates, the subject of the next step, generate files. They live in separate sections of the form and never interact. If you are trying to name something, you want this step; if you are trying to produce a .sql file, you want the next one.


Step 6 - Attach code templates to a type definition

A type definition can also carry code generation templates. Any model object that references the type then knows exactly how to generate itself - so you can, for example, make every entity that is typed HistoricalEntity produce a table script from one shared template, without configuring generation object by object.

Open the definition in the form editor and scroll to the Code Generation Templates section. Add a template row - the row opens in a popup where you fill in three fields:

Templates section on an entity definition

Field Purpose
Path The template file to render. Pick a .handlebars / .hbs or .njk / .nunjucks file.
Output Directory The folder the generated file is written to.
Output Filename The name of the generated file. May contain template expressions.

A few things to know about how these fields behave:

  • Paths are relative to the definition file. Path and Output Directory are resolved from the location of the definition that declares them, so a path like ../../templates/entity/HubTable.handlebars points relative to the definition's own folder.
  • The output filename is rendered per object. Expressions in Output Filename (and Output Directory) are evaluated against each object that is generated. An entity template with HUB_{{entity.id}}.sql produces HUB_Customer.sql, HUB_Product.sql, and so on - a distinct file per entity.
  • The engine is inferred from the file extension - .handlebars / .hbs use Handlebars, .njk / .nunjucks use Nunjucks. There is no separate engine setting.

Templates are inherited too

Templates follow the same inheritance rules as everything else. A definition inherits its parent's templates (matched by their Path), and a child can override individual fields - for example keep the inherited Output Directory but set its own Output Filename. Putting shared templates on an abstract base definition is a clean way to apply them across a whole family of types.

Generating the files

Once a type carries templates, generate from the model objects that use it:

  • Right-click one or more objects in the explorer and choose Generate to generate just those objects.
  • Right-click a data model and choose Generate All to generate every object in the model that has a type with templates.

Generate command in the explorer

CrossModel resolves each object's effective templates (its own plus any inherited), renders them against the object, and writes the results to the configured output paths. A summary reports how many files were written; if an object's type has no templates, it is simply skipped.

Generate command in the explorer

Which objects can be generated

Code generation runs for entity, relationship and data model objects. Templates can also be added to attribute, identifier and custom property definitions, but those are not generated directly - keep generation templates on the definition kinds above.

Generate results in the explorer

For writing the template bodies themselves - the available model properties, custom properties, Handlebars and Nunjucks syntax, and previewing output - see the Code Generation tutorial.


Using typed values downstream

Everything resolved here is the same effective model that code generation sees. Templates read inherited attributes and custom properties transparently - inherited attributes appear in the entity's attribute list, and a custom property is available by its identifier. If you ever need only the values declared locally on an object, the raw view is reachable through _local.

This is what makes type definitions powerful end to end: define a pattern once, apply it with a single type reference, and have it flow consistently into every perspective - form, diagram, and generated code.


Where definitions are stored

Type definitions are plain files in your workspace, so they are versioned in Git alongside the rest of your models. They live in a definitions/ folder inside a data model, with a sub-folder per kind:

YourDataModel/
├── datamodel.cm
├── definitions/
│   ├── entities/          *.entity-definition.cm
│   ├── attributes/        *.attribute-definition.cm
│   ├── relationships/     *.relationship-definition.cm
│   ├── identifiers/       *.identifier-definition.cm
│   ├── datamodels/        *.datamodel-definition.cm
│   └── customproperties/  *.customproperty-definition.cm

You normally create and edit definitions through the form editor, but you can also open any of these files in the code view to edit them as text.


Wrapup

In this tutorial you extended CrossModel's metamodel with your own types. You created a custom property definition, organised definitions into a hierarchy with extends, applied a type to a model object, and saw how CrossModel resolves and overrides inherited values across the definition chain and the instance. From here, combine type definitions with code generation to turn your house standards into generated SQL, configuration, and documentation.