Skip to content

5. Actions, Rules, and Lifecycles

Fields describe state. Actions describe intent to change state. A lifecycle declares which state changes are legal.

text
Action request
  -> availability and before-commit rules
  -> legal lifecycle transition
  -> state mutation
  -> declared events and effects
  -> audit and replay capture

Declare a lifecycle on its owning facet

dart
enum AreaStatus { draft, active, retired }

abstract final class AreaFields {
  static const status = StatusField(
    'status',
    AreaStatus.values,
    dbEnumName: 'area_status',
  );
}

final governanceFacet = Facet(
  name: 'governance',
  title: 'Governance',
  lifecycle: Lifecycle(
    stateField: AreaFields.status,
    initialState: 'draft',
    transitions: const [
      Transition(from: 'draft', to: 'active', trigger: 'activate'),
      Transition(from: 'active', to: 'retired', trigger: 'retire'),
    ],
  ),
  actions: const [
    // Added below.
  ],
);

Transition.trigger is the name of an action on the same facet. The validator checks state and trigger references. Transition.anyState ('*') is available for deliberately global transitions.

Put the complete decision surface on the action

dart
const activateArea = Action(
  name: 'activate',
  title: 'Activate',
  description: 'Make the area available for controlled operations.',
  scope: ActionScope.record,
  consistency: ConsistencyMode.strict,
  rules: [
    Rule(
      id: 'ops.area.activate.grant',
      kind: RuleKind.access,
      phase: RulePhase.availability,
      condition: ActorHasGrantCondition(
        grant: 'ops.area.approve',
      ),
      error: ActionError(
        code: 'ops.area.activate.not_allowed',
        kind: ActionErrorKind.access,
        message: 'The actor cannot activate areas in this scope.',
      ),
    ),
    Rule(
      id: 'ops.area.activate.lifecycle',
      kind: RuleKind.lifecycle,
      condition: StateIsCondition(
        facet: 'governance',
        state: 'draft',
      ),
    ),
  ],
  audit: AuditEnvelope(
    requireSignature: true,
    requireReasonCode: true,
  ),
  emits: [
    ActionEvent(
      name: 'ops.area.activated',
      kind: ActionEventKind.stateChanged,
      facet: 'governance',
    ),
  ],
  capture: ActionCapture.standard,
  ui: ActionUI(
    icon: UIIcon('activate'),
    confirmationMessage: 'Activate this area?',
  ),
);

Attach it to governanceFacet.actions. An action belongs to the facet whose state and behavior it governs.

Conditions are typed data

The shipped condition vocabulary includes:

  • constants: AlwaysCondition, NeverCondition;
  • row predicates: PredicateCondition, FieldEqualsCondition, ExistsCondition;
  • actors: ActorInSetCondition, ActorHasGrantCondition, ActorQualifiedCondition;
  • policy and lifecycle: PolicyAllowsCondition, StateIsCondition;
  • proof: EvidencePresentCondition;
  • boolean composition: AllCondition, AnyCondition, NotCondition;
  • an escape hatch: EvaluatorCondition(ref: 'ops.area.release').

Prefer built-in conditions. A named evaluator remains explainable only when its reference and implementation version are captured at runtime.

Rule phase and severity are independent

dart
Rule(
  id: 'ops.area.capacity.warning',
  kind: RuleKind.data,
  phase: RulePhase.beforeCommit,
  severity: RuleSeverity.warning,
  condition: const ExistsCondition(path: 'hierarchy.capacity'),
)
  • availability powers disabled/hidden action affordances and preflight.
  • beforeCommit guards the transaction.
  • afterCommit verifies the committed outcome.
  • async schedules deferred evaluation.

info and warning findings are still captured. blocking prevents commit.

Events are facts; effects are consequences

dart
Action(
  name: 'activate',
  emits: const [
    ActionEvent(
      name: 'ops.area.activated',
      kind: ActionEventKind.stateChanged,
      required: true,
    ),
  ],
  effects: const [
    Effect(
      targetEntity: 'equipment',
      targetFacet: 'availability',
      targetTrigger: 'refresh',
      consistencyOverride: ConsistencyMode.eventual,
    ),
  ],
)

The event says what happened. The effect asks another surface to react. Eventual effects leave through the durable outbox; strict effects participate in the same transaction.

Shipped compatibility boundary

Action is the public declaration. Today Action.toTrigger() lowers availability and before-commit rules to the compact trigger contract used by parts of the current runtime. Do not author new domains directly against Trigger unless you are implementing a low-level adapter.

Checkpoint

Add a retire action that:

  1. requires ops.area.approve;
  2. is legal only from active;
  3. requires a reason and signature;
  4. emits ops.area.retired;
  5. produces an eventual refresh effect.

Validate that the transition names match actions on the facet.

Next: Modules and assembly.

Reference: Vocabulary — actions and execution.

Blue is the Vyuh Blueprint documentation surface.