The Meta Model
Core Principle
Everything in the system is an Entity. The meta model is fully self-describing -- the entity types, relationship types, property types, lifecycle definitions, and trait types are all themselves entities, stored and queryable through the same API.
This means the same mechanisms used to create, read, update, and version business data also apply to the schema that defines that data. There is no separate "schema layer" -- the meta model is data.
An EntityType called recipe is itself an entity of type entity_type/entity_type. A PropertyType called system_name is an entity of type entity_type/property_type. The system uses itself to describe itself.
Immutability and Versioning
Entities are immutable. Once created, an entity record is never modified in place. Any change -- updating a property, adding a relationship, attaching a trait -- produces an entirely new entity with a new id. The new entity's metadata.previous points back to the entity it was derived from, forming an append-only version chain.
Two identifiers distinguish a specific snapshot from a living entity:
| Identifier | Changes on update? | Description |
|---|---|---|
id | Yes | UUID of this immutable snapshot. Changes with every new version. |
root_id | No | Stable identity across all versions. Equal to id on creation; use this as the persistent reference to an entity. |
graph LR
V1["v1 (id: aaa...)"] -->|"metadata.previous"| V2["v2 (id: bbb...)"]
V2 -->|"metadata.previous"| V3["v3 (id: ccc...)"]
subgraph identity ["root_id = aaa..."]
V1
V2
V3
end
All three versions share the same root_id (the id of the first version). Looking up an entity by root_id always returns the latest version.
Entity Fields
Every entity carries the following fields:
| Field | Description |
|---|---|
id | UUID -- uniquely identifies this immutable snapshot (changes with every new version) |
type_id | UUID of the entity's EntityType |
root_id | Stable identity across versions -- UUID of the first version |
@key | Human-readable string, unique within the entity's type; acts as a stable alias for root_id |
@self | Canonical reference string in type_name/key format |
@label | Computed display label (result of label_script, falls back to @self) |
@status | Current lifecycle status (absent if no lifecycle map is assigned) |
metadata | System metadata (see below) |
properties | Key-value map of the entity's property values |
relationships | Map of relationship name to list of related entity UUIDs |
lists | Map of list name to list of sub-entity entries |
traits | Map of trait name to trait property values |
Metadata
Every entity carries a metadata object:
{
"metadata": {
"previous": "00000000-0000-0000-0000-000000000000",
"created_by": "system",
"created_on": "2026-02-25T15:05:29+01:00",
"package_name": "ep.core"
}
}
| Field | Description |
|---|---|
previous | UUID of the entity this version was derived from (00000000... for the first version) |
created_by | Who created this version |
created_on | ISO 8601 timestamp of creation |
package_name | The package this entity belongs to |
The Key
The key is a human-readable string that uniquely identifies an entity within a key space. Uniqueness is strictly enforced by the engine at write time and is checked only against the latest version of each entity in the key space.
Key Resolution
The key is derived when an entity is created or updated. Resolution follows this precedence:
- Explicit
keyin the payload -- used as-is. key_scripton the entity type -- evaluated if no explicit key is provided (see Lua Scripting).- Stringified entity
id-- used as a fallback if neither of the above yields a value.
Key Spaces
A key space is the engine's uniqueness scope. The key space an entity's key is checked in depends on the kind of entity:
| Entity kind | Key space |
|---|---|
| Regular entity instance | The type entity's key space (e.g. all vegetable instances share a key space) |
| Entity type | The key space of entity_type/entity_type (recursive base case) |
| Trait instance | The trait type entity's key space (globally scoped -- unique values are enforced across all entities that carry the trait) |
| Relationship instance | A sub key space scoped to (relationship_type, source entity) |
| List-item entity | A sub key space scoped to (list-item type, owner entity) |
Sub key spaces ensure that relationship and list-item instances scoped to different source/owner entities never collide with each other.
Concrete examples:
- Two
vegetableinstances cannot share a key. - A relationship keyed
mainonrecipe/soupdoes not collide with one keyedmainonrecipe/salad. - A list item keyed
step1underprocedure/Adoes not collide withstep1underprocedure/B.
Sub key spaces also handle type inheritance: if two relationship types share a common ancestor via has_parent, they resolve to the same key space, enforcing uniqueness of inherited properties across sibling types.
Key spaces are an internal mechanism -- they do not appear in the wire format.
Key Changes on Update
The key is re-derived each time a new version is created:
- Key unchanged -- the key is updated to point at the new version; the old version is only reachable by its
id. - Key changed -- the new key points at the new version; the old key remains valid and continues to point at the old version. Both keys coexist in the same key space.
Addressing an Entity
There are three stable identifiers for an entity, each serving a different purpose:
| Identifier | Format | Resolves to | Use when |
|---|---|---|---|
@self / type_name/key | vegetable/potato | Latest version | Default -- preferred for all references |
root_id | UUID | Latest version | No key exists, or an API requires a UUID |
id | UUID | Exact version | A specific immutable snapshot must be pinned |
@self is always type_name/key and is the canonical way to refer to a living entity. root_id is the stable UUID equivalent. id is version-specific and should only be used when an exact historical snapshot is required.
Relationship instances follow the same pattern, with @self taking the form source_type/source_key/relationship_type/relationship_key (e.g. vegetable/potato/harvest_location/kansas).
API Lookup Prefixes
The REST API offers four equivalent ways to address a single entity:
| Pattern | Resolves by |
|---|---|
/api/v1/by-id/<uuid> | Exact entity version UUID |
/api/v1/by-root/<uuid> | Root UUID -- always resolves to the latest version |
/api/v1/by-key/<type_name>/<key> | Type-scoped key -- always resolves to the latest version |
/api/v1/by-type/<type_name>/<key> | Alias for by-key |
All four prefixes accept the same sub-paths. See REST API Reference for the full route documentation.
Entity References
Entities are referenced using the canonical type_name/key format. Examples:
entity_type/reciperelationship_type/has_ingredientstring_property_type/genericlc_map/EP_Default
This format is used consistently in API request bodies, relationship targets, and configuration.