Preskoči na vsebino

Entities

The data nucleus of every Orbital Unit: typed fields, persistence modes, and the binding system that connects data to behavior.

Related: Traits | Pages | Closed Circuit


What Is an Entity?

An Entity is the data definition at the center of an Orbital Unit. It declares the shape of the data that traits operate on, pages display, and effects modify. Every Orbital Unit has exactly one primary entity.

Orbital Unit = Entity + Traits + Pages

The entity defines what data exists. Traits define how it changes. Pages define where users see it. These three parts compose into a self-contained unit of functionality.

ContactContactFlowStatusTrackingContactListPageContactDetailPage

An entity declaration looks like this:

type ContactStatus = active | inactive | archived

entity Contact [persistent: contacts] {
id : string!
fullName : string!
email : string
status : ContactStatus
tags : [string]
createdAt : string
}

Entity Properties

PropertyRequiredDescription
nameYesPascalCase identifier (e.g., Contact, Order, GameState)
persistenceNoStorage mode: persistent (default), runtime, or singleton
collectionFor persistentDatabase collection name (e.g., contacts, orders)
fieldsYesArray of field definitions

Field Types

Every field has a name, a type, and optional modifiers. The .orb language supports these field types:

TypeDescriptionExample ValueCompiles To
stringText data"hello"string
numberNumeric values (float)42.5number
booleanTrue/falsetrueboolean
dateDate without time"2026-03-01"Date
datetimeDate with time"2026-03-01T10:30:00Z"Date
timestampMilliseconds since epoch1709312400000number
enumNamed constants from a fixed set"pending"Union type
arrayOrdered collection["a", "b"]T[]
objectStructured nested data{ key: "value" }Record<string, unknown>
relationReference to another entity"user_123"string (FK)

Field Properties

Each field accepts these properties:

PropertyDescription
namecamelCase field identifier
typeOne of the types listed above
requiredWhether the field must have a value
primaryKeyDesignates the primary key field
uniqueEnforces uniqueness constraint
defaultDefault value (literal or S-expression)
valuesFor enum type: array of allowed values
itemsFor array type: element type definition
propertiesFor object type: nested field definitions
relationFor relation type: target entity and cardinality

Enum Fields

Enum fields restrict values to a predefined set. Declare a named type alias with |-separated variants:

type Priority = low | medium | high | critical

entity Task [persistent: tasks] {
priority : Priority = "medium"
}

The compiler generates a TypeScript union type: "low" | "medium" | "high" | "critical".

Array and Object Fields

Array fields use bracket syntax [elementType]:

entity Post [persistent: posts] {
tags : [string]
scores : [number]
}

Object fields use the object type:

entity Profile [persistent: profiles] {
id : string!
address : object
}

Relation Fields

Relations link entities together. A one relation is a string FK; a many relation is an array of string IDs:

entity Task [persistent: tasks] {
id : string!
assigneeId : string // FK to User (one)
reviewerIds : [string] // FKs to User (many)
}

Cardinality options:

  • string field: single reference (foreign key)
  • [string] field: multiple references (array of IDs)

Persistence Modes

The persistence property controls where entity data lives and how it is shared. This choice fundamentally affects storage, lifetime, and isolation.

persistent (default)

Data is stored in a database (Firestore, PostgreSQL, or another adapter). It survives restarts and is shared across all sessions. Requires a collection name.

type OrderStatus = pending | paid | shipped

entity Order [persistent: orders] {
id : string!
total : number
status : OrderStatus
}

Use persistent entities for domain objects that must outlive a single session: users, orders, products, invoices, posts.

All orbitals referencing the same entity name share the same collection. If Orbital A and Orbital B both define an entity called Order, they read and write the same database records.

runtime

Data exists only in memory for the duration of the session. No database, no collection. Lost on restart.

entity Particle [runtime] {
id : string
x : number = 0
y : number = 0
velocity : number = 1
}

Use runtime entities for temporary, session-scoped data: game enemies, particles, draft form state, undo history. Each orbital gets its own isolated instances. Orbital A's runtime entities are invisible to Orbital B.

singleton

A single instance shared across all orbitals. Stored in memory, one record only. No collection needed.

type Theme = light | dark

entity AppConfig [singleton] {
id : string
theme : Theme = "light"
language : string = "en"
debugMode : boolean = false
}

Use singleton entities for global state that every orbital should see and modify: player profile in a game, app-wide configuration, current user session. Accessed in S-expressions via @EntityName (e.g., @AppConfig.theme).

Comparison

Aspectpersistentruntimesingleton
StorageDatabaseMemoryMemory
LifetimePermanentSessionSession
SharingShared by entity nameIsolated per orbitalSingle instance, global
CollectionRequiredNoneNone
Typical useDomain objectsTemporary/game entitiesGlobal config, player state

Live Example: Entity with Browse

This complete .orb program defines a BrowseItem entity with five fields and a single-state trait that renders them in a data grid. The entity uses runtime persistence (in-memory, session-scoped) and demonstrates string fields with an enum-like status field constrained by a values array. Ten seed instances are provided so the grid has data to display immediately.

orbital BrowseItemOrbital {
entity BrowseItem [runtime] {
id : string
name : string
description : string
status : string
createdAt : string
}
trait BrowseItemBrowse -> BrowseItem [interaction] {
state browsing {
INIT -> browsing
(ref BrowseItem)
(render-ui main { type: "stack", direction: "vertical", gap: "lg", className: "max-w-5xl mx-auto w-full", children: [{ type: "stack", direction: "horizontal", gap: "md", justify: "space-between", align: "center", children: [{ type: "stack", direction: "horizontal", gap: "sm", align: "center", children: [{ type: "icon", name: "list", size: "lg" }, { type: "typography", content: "BrowseItems", variant: "h2" }] }] }, { type: "divider" }, { type: "data-grid", entity: "BrowseItem", emptyIcon: "inbox", emptyTitle: "No browseitems yet", emptyDescription: "Create your first browseitem to get started.", columns: [{ name: "name", label: "Name", variant: "h4", icon: "list" }, { name: "description", label: "Description", variant: "badge", colorMap: { active: "success", completed: "success", done: "success", pending: "warning", draft: "warning", scheduled: "warning", inactive: "neutral", archived: "neutral", disabled: "neutral", error: "destructive", cancelled: "destructive", failed: "destructive" } }, { name: "status", label: "Status", variant: "caption" }] }, { type: "floating-action-button", icon: "plus", event: "INIT", label: "Create", tooltip: "Create" }] })
}
}
page "/browseitems" -> BrowseItemBrowse
}
Loading preview...

Bindings

Bindings are how S-expressions (guards, effects) access entity data at runtime. Every binding starts with @ and resolves to a value from the current execution context.

Core Binding Roots

BindingResolves ToExample
@entityThe current entity instance being processed@entity.status, @entity.id
@payloadData attached to the incoming event@payload.newTitle, @payload.amount
@stateName of the current trait state (string)@state returns "active"
@nowCurrent timestamp in milliseconds@now returns 1709312400000
@userAuthenticated user information@user.id, @user.email
@EntityNameSingleton entity instance (by name)@AppConfig.theme, @Player.health

These are the valid binding roots. @result is not a binding root; call-service results flow through the runtime differently.

Bindings in Guards

Guards use bindings to evaluate conditions before allowing a transition:

state active {
COMPLETE -> completed
when (and (>= @entity.progress 100) (= @entity.assigneeId @user.id))
}

This transition only fires if progress is at least 100 AND the current user is the assignee. If the guard fails, the transition is blocked and no effects execute.

Bindings in Effects

Effects use bindings to read data and write changes:

SET_STATUS -> active
(set @entity.status @payload.newStatus)
(set @entity.updatedAt @now)
(set @entity.score (+ @entity.score 10))

The first effect reads @payload.newStatus and writes it to the entity's status field. The third uses an S-expression to increment score by 10.

Dot Notation for Nested Access

Bindings support dot-separated paths to reach nested values:

@entity.address.city        → entity.address.city
@payload.metadata.tags → payload.metadata.tags
@Player.inventory.slots → Player (singleton).inventory.slots

How Binding Resolution Works

  1. Parse: extract the @ prefix and root name
  2. Lookup: check local bindings (from let expressions), then core binding roots
  3. Navigate: follow dot path through the object structure
  4. Return: the resolved value, or undefined if the path does not exist

Live Example: Bindings in Action

This program demonstrates @entity, @payload, and @state bindings. The counter trait uses @entity.count to display the current value and S-expression math (["+", "@entity.count", 1]) to increment it.

;; app CounterApp

orbital CounterUnit {
entity Counter [runtime] {
id : string!
count : number
label : string
}
trait CounterTrait -> Counter [interaction] {
state Counting {
INIT -> Counting
(render-ui main { type: "stack", direction: "vertical", gap: "lg", align: "center", children: [{ type: "typography", content: "@entity.count", variant: "h1" }, { type: "stack", direction: "horizontal", gap: "md", children: [{ type: "button", label: "+1", event: "INCREMENT", variant: "primary" }, { type: "button", label: "-1", event: "DECREMENT", variant: "secondary" }, { type: "button", label: "Reset", event: "RESET", variant: "outline" }] }] })
INCREMENT -> Counting
(set @entity.count (+ @entity.count 1))
DECREMENT -> Counting
when (> @entity.count 0)
(set @entity.count (- @entity.count 1))
RESET -> Counting
(set @entity.count 0)
}
}
page "/counter" -> CounterTrait
}
;; app CounterApp

orbital CounterUnit {
entity Counter [runtime] {
id : string!
count : number
label : string
}
trait CounterTrait -> Counter [interaction] {
state Counting {
INIT -> Counting
(render-ui main { type: "stack", direction: "vertical", gap: "lg", align: "center", children: [{ type: "typography", content: "Counter", variant: "h2" }, { type: "typography", content: "0", variant: "h1" }, { type: "stack", direction: "horizontal", gap: "md", children: [{ type: "button", label: "+1", event: "INCREMENT", variant: "primary" }, { type: "button", label: "-1", event: "DECREMENT", variant: "secondary" }, { type: "button", label: "Reset", event: "RESET", variant: "outline" }] }] })
INCREMENT -> Counting
(set @entity.count (+ @entity.count 1))
DECREMENT -> Counting
(set @entity.count (- @entity.count 1))
RESET -> Counting
(set @entity.count 0)
}
}
page "/counter" -> CounterTrait
}
Loading preview...

Trait-Entity Binding (linkedEntity)

Traits are state machines. Each trait operates on one entity. The connection between a trait and its entity is explicit through linkedEntity.

Default Binding

Every orbital has a primary entity defined in its entity property. Traits that omit linkedEntity default to this primary entity:

orbital TaskManager {
entity Task [persistent: tasks] { ... }
trait StatusTrait -> Task [interaction] { ... }
}

Here, StatusTrait explicitly binds to Task via -> Task. All @entity bindings inside StatusTrait resolve to Task data.

Explicit Binding

When a trait needs to operate on a different entity, specify linkedEntity:

orbital ProjectDashboard {
entity Project [persistent: projects] { ... }
trait ProjectOverview -> Project [interaction] { ... }
trait MemberList -> Member [interaction] { ... }
trait ActivityFeed -> Activity [interaction] { ... }
}

Each trait targets a different entity. @entity inside MemberList resolves to Member data, not Project.

Why This Matters

  1. Reusable traits: a generic StatusManagement trait can work with any entity that has a status field
  2. Cross-entity operations: a single orbital can coordinate multiple entity types
  3. Type safety: the compiler validates that fields referenced in @entity.fieldName actually exist on the linked entity

Entity Sharing and Isolation

How entities interact across orbitals depends on the persistence mode.

Persistent entities share by name. If two orbitals both define an entity named Task with "persistence": "persistent", they read and write the same database collection. Changes made in one orbital are visible in the other.

Orbital A (entity: Task, persistent) ──┐
├──► Collection: "tasks"
Orbital B (entity: Task, persistent) ──┘

Runtime entities are isolated. Each orbital gets its own separate instances in memory:

Orbital A (entity: Enemy, runtime) ──► Memory: isolated set A
Orbital B (entity: Enemy, runtime) ──► Memory: isolated set B

Singleton entities are global. One instance, shared by every orbital:

Orbital A ──┐
Orbital B ──┼──► Single AppConfig instance
Orbital C ──┘

Live Example: Enum Fields and Persistence Modes

This program shows an entity with string fields constrained by values arrays, simulating enum behavior. The status field accepts only active, inactive, or pending. The browse trait renders all instances in a data grid with columns for name, description, and status. This is the same std-browse behavior pattern used across all Almadar applications for list views.

orbital BrowseItemOrbital {
entity BrowseItem [runtime] {
id : string
name : string
description : string
status : string
createdAt : string
}
trait BrowseItemBrowse -> BrowseItem [interaction] {
state browsing {
INIT -> browsing
(ref BrowseItem)
(render-ui main { type: "stack", direction: "vertical", gap: "lg", className: "max-w-5xl mx-auto w-full", children: [{ type: "stack", direction: "horizontal", gap: "md", justify: "space-between", align: "center", children: [{ type: "stack", direction: "horizontal", gap: "sm", align: "center", children: [{ type: "icon", name: "list", size: "lg" }, { type: "typography", content: "BrowseItems", variant: "h2" }] }] }, { type: "divider" }, { type: "data-grid", entity: "BrowseItem", emptyIcon: "inbox", emptyTitle: "No browseitems yet", emptyDescription: "Create your first browseitem to get started.", columns: [{ name: "name", label: "Name", variant: "h4", icon: "list" }, { name: "description", label: "Description", variant: "badge", colorMap: { active: "success", completed: "success", done: "success", pending: "warning", draft: "warning", scheduled: "warning", inactive: "neutral", archived: "neutral", disabled: "neutral", error: "destructive", cancelled: "destructive", failed: "destructive" } }, { name: "status", label: "Status", variant: "caption" }] }, { type: "floating-action-button", icon: "plus", event: "INIT", label: "Create", tooltip: "Create" }] })
}
}
page "/browseitems" -> BrowseItemBrowse
}
Loading preview...

Summary

Entities are the data foundation of every Orbital Unit:

  1. Typed fields: string, number, boolean, date, enum (with values), array, object, relation
  2. Persistence modes: persistent (database, shared), runtime (memory, isolated), singleton (memory, global)
  3. Binding system: @entity, @payload, @state, @now, @user, @EntityName for S-expression access
  4. Trait binding: linkedEntity connects traits to their data source explicitly
  5. Sharing rules: persistent entities share by name, runtime entities isolate per orbital, singletons are global

Traits operate on entities. Pages display them. The runtime manages their lifecycle. Everything connects through the binding system and the closed circuit.