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:
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 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.
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.
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 list below) |
| Length | Maximum length, for Text / Binary |
| Precision | Total digits, for Decimal / Float |
| Scale | Fractional digits, for Decimal / Time / DateTime |
| Mandatory | Whether instances must provide a value |
| Value | A default value supplied to instances |
| 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.
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:
Now also add custom properties, use the custom property type data owner created in the previous step:
You need to use the popup to set the custom property type:
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:
Notice that the custom properties also show as inherited:
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.
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 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:
- The item's own value — set on the object itself, or inherited through its
extendschain. The most specific (lowest) explicitly-set value wins. - The type definition's value — supplied by the referenced type and that
definition's own
extendschain. - 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 — 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 Templates section. Add a template row — the row opens in a popup where you fill in three fields:
| 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.
PathandOutput Directoryare resolved from the location of the definition that declares them, so a path like../../templates/entity/HubTable.handlebarspoints relative to the definition's own folder. - The output filename is rendered per object. Expressions in
Output Filename(andOutput Directory) are evaluated against each object that is generated. An entity template withHUB_{{entity.id}}.sqlproducesHUB_Customer.sql,HUB_Product.sql, and so on — a distinct file per entity. - The engine is inferred from the file extension —
.handlebars/.hbsuse Handlebars,.njk/.nunjucksuse 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.
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.
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.
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.











