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
entity <EntityId> {
states: [<state>, <state>, ...]
initial: <state>
transitions: [
(<from_state>, <to_state>),
...
]
parent: <EntityId> // optional -- hierarchy
}Fields
| Field | Required | Description |
|---|---|---|
states | Yes | The complete, finite set of valid states. |
initial | Yes | The starting state. Must be a member of states. |
transitions | Yes | Allowed state changes as (from, to) pairs. Both endpoints must be in states. |
parent | No | Parent 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".
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
initialstate. 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:
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 reachableTransitions 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:
- An operation with a matching effect
(Order, pending, confirmed)is invoked. - The operation's persona check passes.
- The operation's precondition evaluates to true.
- The executor validates the entity is actually in the
fromstate (obligation E2).
Entities declare the shape. Operations make things happen.
Full Working Example
A complete contract demonstrating entities with operations and rules:
// --- 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 (
rejectedviadraft -> submitted -> rejected). - S4:
requestorcan only submit;managercan approve submitted or reject;financecan 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 ofS(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.