Skip to main content

Traits

Trait definitions and state machine types for Almadar


How traits work in the Almadar/Orbital architecture - state machines, guards, effects, and cross-orbital communication.

Related: Entities


Overview

In Almadar, a Trait is a state machine that defines behavior for an entity. The fundamental composition is:

Orbital Unit = Entity + Traits + Pages

While Entities define the shape of data, Traits define how that data changes over time through states, transitions, guards, and effects.


Trait Definition

A trait is defined in the .orb schema with the following structure:

{
"name": "TaskManagement",
"category": "interaction",
"linkedEntity": "Task",
"description": "Manages task lifecycle and status changes",
"emits": [
{ "event": "TASK_COMPLETED", "scope": "external" }
],
"listens": [
{ "event": "USER_ASSIGNED", "triggers": "ASSIGN" }
],
"stateMachine": {
"states": [
{ "name": "idle", "isInitial": true },
{ "name": "active" },
{ "name": "completed", "isTerminal": true }
],
"events": [
{ "key": "START", "name": "Start Task" },
{ "key": "COMPLETE", "name": "Complete Task" }
],
"transitions": [
{
"from": "idle",
"to": "active",
"event": "START",
"effects": [["set", "@entity.id", "status", "active"]]
},
{
"from": "active",
"to": "completed",
"event": "COMPLETE",
"guard": ["=", "@entity.assigneeId", "@user.id"],
"effects": [
["set", "@entity.id", "status", "completed"],
["emit", "TASK_COMPLETED", { "taskId": "@entity.id" }]
]
}
]
}
}

Trait Properties

PropertyRequiredDescription
nameYesTrait identifier (PascalCase)
categoryNoTrait category (see below)
linkedEntityNoEntity this trait operates on
descriptionNoHuman-readable description
emitsNoEvents this trait can emit
listensNoEvents this trait listens for
stateMachineYesState machine definition
ticksNoScheduled/periodic effects
configNoConfiguration schema

Trait Categories

Traits are categorized by their primary purpose:

CategoryPurposeTypical Effects
interactionClient-side UI event handlingrender-ui, navigate, notify
integrationServer-side operationspersist, fetch, call-service
lifecycleEntity lifecycle managementpersist, emit
gameCoreGame loop and physicsset, emit, ticks
gameEntityGame entity behaviorsset, emit, render-ui
gameUiGame UI, HUD, controlsrender-ui, notify

Category Examples

Interaction Trait - Handles UI events:

{
"name": "FormInteraction",
"category": "interaction",
"stateMachine": {
"transitions": [{
"event": "SUBMIT",
"effects": [
["render-ui", "main", { "type": "form", "loading": true }],
["emit", "FORM_SUBMITTED", "@payload"]
]
}]
}
}

Integration Trait - Handles server operations:

{
"name": "DataPersistence",
"category": "integration",
"stateMachine": {
"transitions": [{
"event": "SAVE",
"effects": [
["persist", "update", "Task", "@entity.id", "@payload"],
["emit", "DATA_SAVED", { "id": "@entity.id" }]
]
}]
}
}

State Machine

Every trait has a state machine that defines its behavior.

States

States represent the possible conditions of a trait:

{
"states": [
{ "name": "idle", "isInitial": true, "description": "Waiting for input" },
{ "name": "loading", "description": "Fetching data" },
{ "name": "active", "description": "Ready for interaction" },
{ "name": "error", "isTerminal": true, "description": "Error state" }
]
}
PropertyDescription
nameState identifier (lowercase)
isInitialStarting state (exactly one required)
isTerminalNo outgoing transitions expected
descriptionHuman-readable description

Events

Events trigger state transitions:

{
"events": [
{ "key": "INIT", "name": "Initialize" },
{ "key": "SUBMIT", "name": "Submit Form", "payload": [
{ "name": "email", "type": "string", "required": true },
{ "name": "name", "type": "string", "required": true }
]},
{ "key": "ERROR", "name": "Error Occurred" }
]
}
PropertyDescription
keyEvent identifier (UPPER_SNAKE_CASE)
nameDisplay name
payloadExpected payload schema

Transitions

Transitions define how states change in response to events:

{
"transitions": [
{
"from": "idle",
"to": "loading",
"event": "SUBMIT",
"guard": ["and", ["!=", "@payload.email", ""], ["!=", "@payload.name", ""]],
"effects": [
["set", "@entity.id", "email", "@payload.email"],
["persist", "create", "User", "@payload"]
]
},
{
"from": ["loading", "active"],
"to": "error",
"event": "ERROR"
}
]
}
PropertyDescription
fromSource state(s) - string or array
toTarget state (always single)
eventTriggering event key
guardCondition that must pass (optional)
effectsEffects to execute on transition (optional)

Multi-source transitions: Use an array for from to handle the same event from multiple states:

{ "from": ["idle", "error"], "to": "loading", "event": "RETRY" }

Guards

Guards are conditions that must evaluate to true for a transition to occur. They use S-expression syntax.

Guard Operators

CategoryOperators
Comparison=, !=, <, >, <=, >=
Logicand, or, not
Math+, -, *, /, %
Arraycount, includes, every, some

Guard Examples

// Simple equality
["=", "@entity.status", "active"]

// Compound condition
["and",
["!=", "@payload.email", ""],
["!=", "@payload.name", ""]
]

// Numeric comparison
[">=", "@entity.balance", "@payload.amount"]

// Array check
[">", ["count", "@entity.items"], 0]

// User permission
["=", "@entity.ownerId", "@user.id"]

// Complex guard
["and",
["=", "@entity.status", "pending"],
["or",
["=", "@user.role", "admin"],
["=", "@entity.assigneeId", "@user.id"]
]
]

Guard Bindings

Guards can reference data through bindings (see Entity Bindings):

BindingDescription
@entity.fieldCurrent entity field value
@payload.fieldEvent payload field
@stateCurrent trait state name
@user.idAuthenticated user ID
@nowCurrent timestamp

Guard Failure

If a guard evaluates to false:

  1. Transition is blocked
  2. No effects execute
  3. State remains unchanged
  4. Response indicates transitioned: false

Effects

Effects are actions executed when a transition occurs. They use S-expression syntax.

Effect Types

EffectServerClientPurpose
render-uiIgnoredExecutesDisplay pattern to UI slot
navigateIgnoredExecutesRoute navigation
notifyIgnoredExecutesShow notification/toast
fetchExecutesIgnoredQuery database
persistExecutesIgnoredCreate/update/delete data
call-serviceExecutesIgnoredCall external API
emitExecutesExecutesPublish event
setExecutesExecutesModify entity field (supports increment/decrement via S-expressions)

Dual Execution Model

Traits execute on both client and server simultaneously:

┌─────────────────────────────────────────────────────────────┐
│ Client Server │
│ ─────── ────── │
│ render-ui ✓ render-ui → clientEffects │
│ navigate ✓ navigate → clientEffects │
│ notify ✓ notify → clientEffects │
│ fetch ✗ fetch ✓ (queries DB) │
│ persist ✗ persist ✓ (writes DB) │
│ call-service ✗ call-service ✓ (API call) │
│ emit ✓ (EventBus) emit ✓ (cross-orbital)│
│ set ✓ set ✓ │
└─────────────────────────────────────────────────────────────┘

Effect Examples

render-ui - Display a UI pattern:

["render-ui", "main", {
"type": "entity-table",
"entity": "Task",
"columns": ["title", "status", "dueDate"]
}]

persist - Database operations:

// Create
["persist", "create", "Task", "@payload"]

// Update
["persist", "update", "Task", "@entity.id", { "status": "completed" }]

// Delete
["persist", "delete", "Task", "@entity.id"]

fetch - Query data:

["fetch", "Task", { "status": "active", "assigneeId": "@user.id" }]

emit - Publish event:

["emit", "TASK_COMPLETED", { "taskId": "@entity.id", "completedBy": "@user.id" }]

set - Modify field:

["set", "@entity.id", "status", "active"]
["set", "@entity.id", "updatedAt", "@now"]
// Increment/decrement using math operators:
["set", "@entity.id", "score", ["+", "@entity.score", 10]] // Increment by 10
["set", "@entity.id", "health", ["-", "@entity.health", 5]] // Decrement by 5

Note: increment and decrement are not separate effect types. Use the set effect with S-expression math operators (+, -) to modify numeric fields.

navigate - Route change:

["navigate", "/tasks/@entity.id"]

notify - Show notification:

["notify", "Task completed successfully", "success"]

call-service - External API:

["call-service", "email", "send", {
"to": "@entity.email",
"subject": "Task Assigned",
"body": "You have been assigned a new task."
}]

linkedEntity - Trait-Entity Binding

The linkedEntity property specifies which entity a trait operates on.

Primary Entity

Every orbital has a primary entity. Traits without linkedEntity use this entity:

{
"name": "TaskManagement",
"entity": { "name": "Task", "fields": [...] },
"traits": [
{ "name": "StatusTrait" } // Uses Task entity
]
}

Explicit linkedEntity

Specify linkedEntity to operate on a different entity:

{
"name": "TaskManagement",
"entity": { "name": "Task" },
"traits": [
{ "name": "StatusTrait", "linkedEntity": "Task" },
{ "name": "CommentTrait", "linkedEntity": "Comment" },
{ "name": "PlayerStatsTrait", "linkedEntity": "Player" }
]
}

Why linkedEntity?

  1. Reusable traits - A generic trait can work with any entity
  2. Cross-entity operations - Operate on related entities
  3. Type safety - Compiler verifies entity field references
  4. Clear dependencies - Explicit binding improves readability

See Entity Bindings for more details.


Event Communication (emit/listen)

Traits communicate through events, enabling loose coupling between orbitals.

Emitting Events

Declare events a trait can emit:

{
"name": "OrderFlow",
"emits": [
{
"event": "ORDER_CONFIRMED",
"scope": "external",
"description": "Fired when order is confirmed",
"payload": [
{ "name": "orderId", "type": "string" },
{ "name": "items", "type": "array" }
]
}
]
}

Emit in effects:

["emit", "ORDER_CONFIRMED", { "orderId": "@entity.id", "items": "@entity.items" }]

Listening for Events

Declare events a trait listens for:

{
"name": "InventorySync",
"listens": [
{
"event": "ORDER_CONFIRMED",
"triggers": "RESERVE_STOCK",
"scope": "external",
"payloadMapping": {
"items": "@payload.items"
},
"guard": [">", ["count", "@payload.items"], 0]
}
]
}
PropertyDescription
eventEvent name to listen for
triggersInternal event to trigger (defaults to event name)
scopeinternal (same orbital) or external (cross-orbital)
payloadMappingTransform incoming payload
guardOptional condition to filter events

Event Scope

ScopeDescription
internalEvents within the same orbital only
externalEvents can cross orbital boundaries

Cross-Orbital Communication Flow

┌──────────────────┐         ┌──────────────────┐
│ OrderManagement │ │ InventoryManagement│
│ │ │ │
│ ┌────────────┐ │ emit │ ┌────────────┐ │
│ │ OrderFlow │──┼────────►│ │InventorySync│ │
│ └────────────┘ │ ORDER_ │ └────────────┘ │
│ │CONFIRMED│ │
└──────────────────┘ └──────────────────┘
  1. OrderFlow trait emits ORDER_CONFIRMED (external scope)
  2. Event bus broadcasts to all listening traits
  3. InventorySync receives event, maps payload
  4. RESERVE_STOCK event triggers on InventorySync
  5. State machine processes transition normally

Ticks (Scheduled Effects)

Ticks run effects periodically, even without user interaction.

Tick Definition

{
"ticks": [
{
"name": "cleanup_expired",
"interval": "60000",
"guard": [">", ["count", "@entity.expiredSessions"], 0],
"effects": [
["persist", "delete", "Session", { "expiresAt": ["<", "@now"] }]
],
"description": "Clean up expired sessions every minute"
},
{
"name": "sync_status",
"interval": "5000",
"effects": [
["fetch", "ExternalStatus", {}],
["set", "@entity.id", "lastSync", "@now"]
]
}
]
}

Tick Properties

PropertyDescription
nameTick identifier
intervalMilliseconds, or string like "5s", "1m"
guardCondition (tick skipped if false)
effectsEffects to execute
appliesToSpecific entity IDs (optional)
descriptionHuman description

Common Tick Patterns

Cleanup:

{
"name": "cleanup",
"interval": "300000",
"effects": [["persist", "delete", "TempData", { "createdAt": ["<", ["-", "@now", 86400000]] }]]
}

Periodic Sync:

{
"name": "sync",
"interval": "10000",
"effects": [
["call-service", "external-api", "fetch-updates", {}],
["emit", "DATA_SYNCED", { "timestamp": "@now" }]
]
}

Game Loop:

{
"name": "game_tick",
"interval": "16",
"effects": [
["set", "@entity.id", "position", ["+", "@entity.position", "@entity.velocity"]],
["render-ui", "canvas", { "type": "game-canvas" }]
]
}

Trait References vs. Inline Traits

Traits can be defined inline or referenced from external sources.

Inline Definition

Define the trait directly in the orbital:

{
"orbital": "TaskManagement",
"traits": [
{
"name": "StatusTrait",
"stateMachine": {
"states": [...],
"transitions": [...]
}
}
]
}

Reference Definition

Reference a trait from the standard library or imports:

{
"orbital": "TaskManagement",
"uses": [
{ "from": "std/behaviors/crud", "as": "CRUD" }
],
"traits": [
{
"ref": "CRUD.traits.CRUDManagement",
"linkedEntity": "Task",
"config": {
"allowDelete": true,
"softDelete": false
}
}
]
}

Reference Properties

PropertyDescription
refPath to trait (e.g., "Alias.traits.TraitName")
linkedEntityOverride entity binding
configConfiguration overrides

When to Use References

  • Reusable patterns - CRUD, authentication, pagination
  • Standard behaviors - From std/behaviors/
  • Cross-project sharing - Import from other schemas
  • Configuration-driven - Same trait, different config

Complete Example

A complete trait demonstrating all features:

{
"name": "CheckoutFlow",
"category": "integration",
"linkedEntity": "Order",
"description": "Handles the checkout process from cart to confirmation",

"emits": [
{ "event": "ORDER_PLACED", "scope": "external", "payload": [
{ "name": "orderId", "type": "string" },
{ "name": "total", "type": "number" }
]},
{ "event": "PAYMENT_FAILED", "scope": "internal" }
],

"listens": [
{ "event": "CART_UPDATED", "triggers": "RECALCULATE", "scope": "internal" },
{ "event": "INVENTORY_RESERVED", "triggers": "CONFIRM_STOCK", "scope": "external" }
],

"stateMachine": {
"states": [
{ "name": "cart", "isInitial": true, "description": "Shopping cart" },
{ "name": "checkout", "description": "Entering shipping/payment" },
{ "name": "processing", "description": "Processing payment" },
{ "name": "confirmed", "description": "Order confirmed" },
{ "name": "failed", "isTerminal": true, "description": "Order failed" }
],

"events": [
{ "key": "PROCEED", "name": "Proceed to Checkout" },
{ "key": "SUBMIT", "name": "Submit Order", "payload": [
{ "name": "paymentMethod", "type": "string", "required": true }
]},
{ "key": "PAYMENT_SUCCESS", "name": "Payment Succeeded" },
{ "key": "PAYMENT_FAILED", "name": "Payment Failed" },
{ "key": "RECALCULATE", "name": "Recalculate Totals" },
{ "key": "CONFIRM_STOCK", "name": "Stock Confirmed" }
],

"transitions": [
{
"from": "cart",
"to": "checkout",
"event": "PROCEED",
"guard": [">", ["count", "@entity.items"], 0],
"effects": [
["render-ui", "main", { "type": "form", "schema": "checkout" }]
]
},
{
"from": "checkout",
"to": "processing",
"event": "SUBMIT",
"guard": ["and",
["!=", "@payload.paymentMethod", ""],
[">=", "@entity.total", 0]
],
"effects": [
["set", "@entity.id", "paymentMethod", "@payload.paymentMethod"],
["set", "@entity.id", "status", "processing"],
["call-service", "payment", "charge", {
"amount": "@entity.total",
"method": "@payload.paymentMethod"
}],
["render-ui", "main", { "type": "stats", "loading": true }]
]
},
{
"from": "processing",
"to": "confirmed",
"event": "PAYMENT_SUCCESS",
"effects": [
["set", "@entity.id", "status", "confirmed"],
["set", "@entity.id", "confirmedAt", "@now"],
["persist", "update", "Order", "@entity.id", "@entity"],
["emit", "ORDER_PLACED", { "orderId": "@entity.id", "total": "@entity.total" }],
["notify", "Order confirmed!", "success"],
["navigate", "/orders/@entity.id"]
]
},
{
"from": "processing",
"to": "failed",
"event": "PAYMENT_FAILED",
"effects": [
["set", "@entity.id", "status", "failed"],
["emit", "PAYMENT_FAILED", { "orderId": "@entity.id" }],
["notify", "Payment failed. Please try again.", "error"]
]
},
{
"from": ["cart", "checkout"],
"to": "cart",
"event": "RECALCULATE",
"effects": [
["set", "@entity.id", "total", ["array/reduce", "@entity.items",
["lambda", ["sum", "item"], ["+", "@sum", "@item.price"]], 0]]
]
}
]
},

"ticks": [
{
"name": "expire_abandoned",
"interval": "300000",
"guard": ["and",
["=", "@state", "checkout"],
["<", "@entity.updatedAt", ["-", "@now", 1800000]]
],
"effects": [
["set", "@entity.id", "status", "abandoned"],
["persist", "update", "Order", "@entity.id", { "status": "abandoned" }]
]
}
]
}

Summary

The Almadar trait system provides:

  1. State Machines - Define possible states and transitions
  2. Guards - Protect transitions with boolean conditions
  3. Effects - Execute actions on transition (UI, database, events)
  4. Dual Execution - Server effects (persist, fetch) + Client effects (render, navigate)
  5. Event Communication - Emit/listen for cross-trait and cross-orbital messaging
  6. Ticks - Scheduled periodic effects
  7. linkedEntity - Explicit binding to entity data
  8. Categories - Classify traits by purpose (interaction, integration, game)
  9. Reusability - Reference traits from libraries or define inline

Traits are the behavioral core of Orbital Units - they define how entities change over time through a declarative, composable state machine model.


Document created: 2026-02-02 Based on codebase analysis of orbital-rust and builder packages