Skip to main content
Version: Next

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.

tip

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:

IdentifierChanges on update?Description
idYesUUID of this immutable snapshot. Changes with every new version.
root_idNoStable 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:

FieldDescription
idUUID -- uniquely identifies this immutable snapshot (changes with every new version)
type_idUUID of the entity's EntityType
root_idStable identity across versions -- UUID of the first version
@keyHuman-readable string, unique within the entity's type; acts as a stable alias for root_id
@selfCanonical reference string in type_name/key format
@labelComputed display label (result of label_script, falls back to @self)
@statusCurrent lifecycle status (absent if no lifecycle map is assigned)
metadataSystem metadata (see below)
propertiesKey-value map of the entity's property values
relationshipsMap of relationship name to list of related entity UUIDs
listsMap of list name to list of sub-entity entries
traitsMap 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"
}
}
FieldDescription
previousUUID of the entity this version was derived from (00000000... for the first version)
created_byWho created this version
created_onISO 8601 timestamp of creation
package_nameThe 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:

  1. Explicit key in the payload -- used as-is.
  2. key_script on the entity type -- evaluated if no explicit key is provided (see Lua Scripting).
  3. 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 kindKey space
Regular entity instanceThe type entity's key space (e.g. all vegetable instances share a key space)
Entity typeThe key space of entity_type/entity_type (recursive base case)
Trait instanceThe trait type entity's key space (globally scoped -- unique values are enforced across all entities that carry the trait)
Relationship instanceA sub key space scoped to (relationship_type, source entity)
List-item entityA 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 vegetable instances cannot share a key.
  • A relationship keyed main on recipe/soup does not collide with one keyed main on recipe/salad.
  • A list item keyed step1 under procedure/A does not collide with step1 under procedure/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.

info

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:

IdentifierFormatResolves toUse when
@self / type_name/keyvegetable/potatoLatest versionDefault -- preferred for all references
root_idUUIDLatest versionNo key exists, or an API requires a UUID
idUUIDExact versionA 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:

PatternResolves 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/recipe
  • relationship_type/has_ingredient
  • string_property_type/generic
  • lc_map/EP_Default

This format is used consistently in API request bodies, relationship targets, and configuration.