Skip to content

An Entity is a finite state machine embedded in a closed-world, well-founded, acyclic partial order. The entity graph is static and finite. Each entity declares a set of states, an initial state, and an explicit transition relation. Entities are the stateful backbone of a Tenor contract -- the things in your domain that change over time.

DSL Syntax

hcl
entity <EntityId> {
  states:      [<state>, <state>, ...]
  initial:     <state>
  transitions: [
    (<from_state>, <to_state>),
    ...
  ]
  parent:      <EntityId>                // optional -- hierarchy
}

Fields

FieldRequiredDescription
statesYesThe complete, finite set of valid states.
initialYesThe starting state. Must be a member of states.
transitionsYesAllowed state changes as (from, to) pairs. Both endpoints must be in states.
parentNoParent entity in the hierarchy DAG. Creates an acyclic partial order.

Single-Instance vs. Multi-Instance

Single-instance (default)

By default, a contract has one runtime instance of each entity type. The executor uses a conventional instance id of "_default".

hcl
entity Order {
  states:  [pending, confirmed, shipped, delivered, cancelled]
  initial: pending
  transitions: [
    (pending, confirmed),
    (pending, cancelled),
    (confirmed, shipped),
    (confirmed, cancelled),
    (shipped, delivered)
  ]
}

Multi-instance (EntityStateMap)

When an entity type has multiple runtime instances (e.g., line items in an order), the executor manages an EntityStateMap -- a mapping from (EntityId, InstanceId) pairs to current states:

EntityStateMap = Map<(EntityId, InstanceId), StateId>

Each InstanceId is a non-empty UTF-8 string that uniquely identifies a runtime instance. The pair (EntityId, InstanceId) is globally unique within a contract's runtime environment. Instance ids are assigned by the executor and are opaque to the contract and evaluator.

The contract declares the entity type and its behavior. The executor determines how many instances exist at any point in time.

Instance lifecycle:

  • Creation (E15): New instances MUST be initialized in the entity's declared initial state. Instances cannot be created in arbitrary states.
  • Identity stability (E16): An InstanceId remains stable for the instance's lifetime. Reuse after deletion is permitted only when no references remain.
  • Enumeration (E17): The EntityStateMap provided to the evaluator must be complete -- every active instance must be present.
  • Deletion: The executor simply omits the instance from the EntityStateMap. The evaluator makes no distinction between "instance was deleted" and "instance never existed."

Parent Hierarchy

Entities may form a directed acyclic graph via parent pointers:

hcl
entity Department {
  states:  [active, archived]
  initial: active
  transitions: [
    (active, archived)
  ]
}

entity Team {
  states:  [active, disbanded]
  initial: active
  transitions: [
    (active, disbanded)
  ]
  parent: Department
}

entity Project {
  states:  [planning, active, completed, cancelled]
  initial: planning
  transitions: [
    (planning, active),
    (active, completed),
    (active, cancelled)
  ]
  parent: Team
}

Hierarchy properties:

  • The entity hierarchy must be acyclic. The transitive closure of the parent relation must be irreflexive. Cycles are elaboration errors.
  • The DAG structure is fixed at contract definition time. No dynamic re-parenting.
  • The hierarchy defines a partial order but carries no implicit authority or conflict semantics. Any propagation across the hierarchy must be explicitly declared, monotonic, and statically analyzable.
  • Propagation evaluation is a single pass over the topologically sorted entity DAG. No fixed-point iteration.

Static Analysis (S1, S2)

The elaborator proves two key properties for every entity:

S1 -- Complete state space. For each entity, the complete set of states is enumerable. The state set is finite, explicitly declared, and fixed at contract definition time.

S2 -- Reachable states. For each entity, every state in the declared set is reachable from the initial state via the declared transition relation. If any state is unreachable, the contract is rejected.

These are structural truths the elaborator proves during compilation, not runtime checks.

Running tenor check on a contract produces:

State Space (S1): 5 states across 1 entities
Reachability (S2): 5/5 states reachable

Transitions Are Permissions, Not Triggers

A declared transition (pending, confirmed) means "it is structurally legal for the entity to move from pending to confirmed." The transition only happens when:

  1. An operation with a matching effect (Order, pending, confirmed) is invoked.
  2. The operation's persona check passes.
  3. The operation's precondition evaluates to true.
  4. The executor validates the entity is actually in the from state (obligation E2).

Entities declare the shape. Operations make things happen.

Full Working Example

A complete contract demonstrating entities with operations and rules:

hcl
// --- Personas ---
persona requestor
persona manager
persona finance

// --- Entities ---
entity Requisition {
  states:  [draft, submitted, manager_approved, finance_approved, rejected]
  initial: draft
  transitions: [
    (draft, submitted),
    (submitted, manager_approved),
    (submitted, rejected),
    (manager_approved, finance_approved),
    (manager_approved, rejected)
  ]
}

// --- Facts ---
fact requisition_total {
  type:   Money(currency: "USD")
  source: "procurement_service.requisition_total"
}

fact budget_available {
  type:   Bool
  source: "budget_service.has_funds"
}

// --- Rules ---
rule budget_ok {
  stratum: 0
  when:    budget_available = true
  produce: verdict budget_ok { payload: Bool = true }
}

// --- Operations ---
operation submit_requisition {
  personas: [requestor]
  require:  requisition_total > 0
  effects:  [Requisition: draft -> submitted]
  outcomes: [submitted]
}

operation manager_approve {
  personas: [manager]
  require:  verdict_present(budget_ok)
  effects:  [Requisition: submitted -> manager_approved]
  outcomes: [approved]
}

operation finance_approve {
  personas: [finance]
  require:  verdict_present(budget_ok)
  effects:  [Requisition: manager_approved -> finance_approved]
  outcomes: [approved]
}

operation reject_requisition {
  personas: [manager, finance]
  require:  true
  effects:  [
    Requisition: submitted -> rejected,
    Requisition: manager_approved -> rejected
  ]
  outcomes: [rejected]
}

Static analysis of this contract confirms:

  • S1: 5 states enumerated for Requisition.
  • S2: All 5 states are reachable (rejected via draft -> submitted -> rejected).
  • S4: requestor can only submit; manager can approve submitted or reject; finance can approve manager_approved or reject.

Constraints

  • The number of entity types |E| is finite and fixed at contract definition time. No dynamic entity type creation.
  • For each entity e, S(e) (the state set) is finite and explicitly enumerated.
  • For each entity e, T(e) (the transition relation) is a subset of S(e) x S(e), finite and explicitly declared.
  • The entity hierarchy forms a directed acyclic graph. Cycle detection uses DFS over parent pointers.
  • The initial state must be a member of the declared states. Violations are Pass 5 errors: "initial state '<state>' is not declared in states: [...]".
  • Every transition endpoint must be a member of the declared states. Violations: "transition endpoint '<state>' is not declared".
  • Entity hierarchy cycles produce: "Entity cycle detected: A -> B -> C -> A".

Common Mistakes

Unreachable states. If you declare a state but no transition path reaches it from initial, the elaborator rejects the contract. Every state must be reachable.

Missing transitions. If you intend for an entity to move from shipped to cancelled, that transition must be declared. Undeclared transitions are impossible -- there is no escape hatch.

Confusing transitions with triggers. A transition (A, B) means "A to B is structurally possible." It does NOT mean "A automatically becomes B when some condition is met." Transitions require operations to execute them.

Assuming entity state is a predicate term. You cannot write Requisition.state = "draft" in a precondition. Entity state is not in the predicate grammar. State constraints are enforced through operation effect declarations and executor obligation E2 (transition source validation).

Creating separate entities when one suffices. If your domain has a single thing moving through states, use one entity. Multiple entities are needed when you have genuinely independent state machines.

How Entities Connect to Other Constructs

  • Operations declare effects that are (EntityId, from_state, to_state) tuples referencing declared entities and transitions.
  • Flows orchestrate operations that transition entities. Parallel branches must have disjoint entity effect sets.
  • Systems can share entities across member contracts via shared_entities. Shared entities must have identical state sets across all sharing contracts.
  • Rules do NOT reference entity state directly. Verdict production depends only on facts and lower-stratum verdicts.

Static Analysis Properties

  • S1 enumerates the complete state space of every entity.
  • S2 verifies every declared state is reachable from the initial state.
  • S3a determines structural admissibility: for each entity state and persona, which operations are structurally possible.
  • S4 derives the authority topology: which personas can cause which transitions.
  • S6 enumerates all flow paths, tracking entity state changes along each path.