[Traits](https://oods-foundry.com/traits) / Stateful

# Stateful

Normalizes lifecycle state transitions, exposes canonical status semantics, and provides optional transition governance for production workflows. Many objects compose Stateful (User, Subscription and Product among them), and it serves as the data source for the Colorized visual trait. At its simplest, Stateful defines an ordered list of valid states and tracks the current status. When governance is enabled via transitionRules, it additionally enforces which transitions are allowed, records the actor and reason for each transition, and materializes the set of valid next states — making it suitable for auditable workflows where arbitrary state jumps are not permitted.

Generated from @oods/foundry 0.10.1

- Group

  lifecycle

- Maturity

  stable

- Contexts

  card, detail, form, list

## Fields it adds

| Field | Type | Required | Description |
| - | - | - | - |
| `status` | `string` | yes | Canonical lifecycle state derived from the states parameter. This is the single source of truth for the entity's current lifecycle position. Consumed by Colorized to resolve visual tokens, and by view extensions to render StatusBadge and StatusTimeline. |
| `state_history` | `StateTransition[]` | no | Chronological log of state transitions. Each entry records the before/after states, timestamp, and (when governance is enabled) the actor, reason, and transition metadata. Rendered by StatusTimeline in the detail and timeline views. Entry structure: - from: string (previous state) - to: string (new state) - timestamp: ISO 8601 datetime - actor_id: string (user/system who triggered the transition, optional) - reason: string (human-readable justification, required when requireTransitionReason is true) - transition_metadata: Record\<string, unknown> (arbitrary context, optional) |
| `allowed_transitions` | `string[]` | no | Materialized list of valid next states from the current status, computed from the transitionRules parameter. When transitionRules is null (open model), this contains all states except the current one. Used by StatusSelector to disable invalid options and by StatusBadge to indicate available paths. |

## What it shows in each context

- [list](https://oods-foundry.com/contexts/list)

  `StatusBadge`

- [detail](https://oods-foundry.com/contexts/detail)

  `StatusTimeline`

- [form](https://oods-foundry.com/contexts/form)

  `StatusSelector`

- [card](https://oods-foundry.com/contexts/card)

  `StatusBadge`

## Parameters

- `states` `string[]`, required

  Ordered list of valid lifecycle states exposed to consumers. The order is semantic — it represents the typical forward progression (e.g., draft → active → archived). All status values must be one of these states.

- `initialState` `string`, required

  State assigned when an entity is created. Must be a member of the states parameter. Typically the first entry in the states list (e.g., "draft").

- `transitionRules` `Record<string, string[]>`

  Allowed state transition map. Keys are source states; values are arrays of permitted target states. When provided, the system enforces that only declared transitions are valid — any other transition is rejected. When omitted, all transitions between any two states in the states parameter are allowed (open model). Example for a subscription lifecycle: draft: \[active, archived] active: \[paused, archived] paused: \[active, archived] archived: \[]

- `requireTransitionReason` `boolean`

  When true, every state transition must include a human-readable reason in the state_history entry. Enforced at the application layer — transitions without a reason field are rejected. Useful for compliance and audit trails.

## Objects that use it

- [Article](https://oods-foundry.com/objects/article)
- [Media](https://oods-foundry.com/objects/media)
- [Organization](https://oods-foundry.com/objects/organization)
- [Product](https://oods-foundry.com/objects/product)
- [Relationship](https://oods-foundry.com/objects/relationship)
- [Subscription](https://oods-foundry.com/objects/subscription)
- [Transaction](https://oods-foundry.com/objects/transaction)
- [User](https://oods-foundry.com/objects/user)

## The trait file

`traits/lifecycle/Stateful.trait.yaml`

```
trait:
  name: Stateful
  version: 2.0.0
  description: |
    Normalizes lifecycle state transitions, exposes canonical status semantics, and provides
    optional transition governance for production workflows. Many objects compose Stateful
    (User, Subscription and Product among them), and it serves as the data source for the
    Colorized visual trait.

    At its simplest, Stateful defines an ordered list of valid states and tracks the current
    status. When governance is enabled via transitionRules, it additionally enforces which
    transitions are allowed, records the actor and reason for each transition, and materializes
    the set of valid next states — making it suitable for auditable workflows where arbitrary
    state jumps are not permitted.
  category: lifecycle
  tags:
    - lifecycle
    - status
    - workflow
    - state
    - governance
    - audit

parameters:
  - name: states
    type: string[]
    required: true
    description: |
      Ordered list of valid lifecycle states exposed to consumers. The order is semantic —
      it represents the typical forward progression (e.g., draft → active → archived).
      All status values must be one of these states.
    default:
      - draft
      - active
      - paused
      - archived

  - name: initialState
    type: string
    required: true
    description: |
      State assigned when an entity is created. Must be a member of the states parameter.
      Typically the first entry in the states list (e.g., "draft").
    default: draft
    validation:
      enumFromParameter: states

  - name: transitionRules
    type: "Record<string, string[]>"
    required: false
    description: |
      Allowed state transition map. Keys are source states; values are arrays of permitted
      target states. When provided, the system enforces that only declared transitions are
      valid — any other transition is rejected. When omitted, all transitions between any
      two states in the states parameter are allowed (open model).

      Example for a subscription lifecycle:
        draft: [active, archived]
        active: [paused, archived]
        paused: [active, archived]
        archived: []
    default: null

  - name: requireTransitionReason
    type: boolean
    required: false
    description: |
      When true, every state transition must include a human-readable reason in the
      state_history entry. Enforced at the application layer — transitions without a
      reason field are rejected. Useful for compliance and audit trails.
    default: false

schema:
  status:
    type: string
    required: true
    description: |
      Canonical lifecycle state derived from the states parameter. This is the single source
      of truth for the entity's current lifecycle position. Consumed by Colorized to resolve
      visual tokens, and by view extensions to render StatusBadge and StatusTimeline.
    validation:
      enumFromParameter: states

  state_history:
    type: StateTransition[]
    required: false
    description: |
      Chronological log of state transitions. Each entry records the before/after states,
      timestamp, and (when governance is enabled) the actor, reason, and transition metadata.
      Rendered by StatusTimeline in the detail and timeline views.

      Entry structure:
        - from: string (previous state)
        - to: string (new state)
        - timestamp: ISO 8601 datetime
        - actor_id: string (user/system who triggered the transition, optional)
        - reason: string (human-readable justification, required when requireTransitionReason is true)
        - transition_metadata: Record<string, unknown> (arbitrary context, optional)
    default: []

  allowed_transitions:
    type: string[]
    required: false
    description: |
      Materialized list of valid next states from the current status, computed from the
      transitionRules parameter. When transitionRules is null (open model), this contains
      all states except the current one. Used by StatusSelector to disable invalid options
      and by StatusBadge to indicate available paths.
    default: []

semantics:
  status:
    semantic_type: status.state
    token_mapping: tokenMap(status.state.*)
    ui_hints:
      component: StatusBadge
      showIcon: true
  state_history:
    semantic_type: timeline.state_transition
    token_mapping: tokenMap(timeline.status.*)
    ui_hints:
      component: StatusTimeline
      collapseWhenEmpty: true
  allowed_transitions:
    semantic_type: status.navigation
    token_mapping: computed
    ui_hints:
      component: StatusSelector
      description: Drives the set of enabled options in the state transition UI.

view_extensions:
  list:
    - component: StatusBadge
      props:
        field: status
        tone: lifecycle
  detail:
    - component: StatusTimeline
      position: top
      priority: 40
      props:
        field: status
        historyField: state_history
        statesParameter: states
        showActorId: true
        showReason: true
  form:
    - component: StatusSelector
      position: top
      props:
        field: status
        optionsParameter: states
        initialParameter: initialState
        allowedTransitionsField: allowed_transitions
        requireReasonParameter: requireTransitionReason
  # s222-m03: no timeline recipe. A timeline's rail lists the state history itself (recordCollectionEvents).
  card:
    - component: StatusBadge
      position: before
      props:
        field: status
        variant: subtle

tokens:
  status.state.default.bg: "var(--status-neutral-bg)"
  status.state.default.text: "var(--status-neutral-text)"
  status.timeline.connector: "var(--border-subtle)"
  status.timeline.node.bg: "var(--sys-surface-raised)"
  status.timeline.node.border: "var(--sys-border-default)"
  status.timeline.actor.text: "var(--sys-text-secondary)"
  status.timeline.reason.text: "var(--sys-text-tertiary)"
  status.timeline.reason.bg: "var(--sys-surface-neutral)"

dependencies: []

metadata:
  created: "2025-10-12"
  updated: "2026-02-28"
  owners:
    - design@oods.systems
    - engineering@oods.systems
  maturity: stable
  composedBy:
    count: 18
    notable:
      - User
      - Subscription
      - Product
      - Order
      - Organization
      - Article
  accessibility:
    keyboard: |
      StatusSelector is keyboard-navigable (arrow keys to cycle states, Enter to confirm).
      Disabled states (not in allowed_transitions) are skipped during keyboard navigation.
    screenreader: |
      StatusBadge announces the current state and, when allowed_transitions is populated,
      the number of available next states (e.g., "Status: Active. 2 transitions available.").
      StateTransitionEvent entries announce from-state, to-state, actor, and reason.
  regionsUsed:
    - list
    - detail
    - form
    - card
  examples:
    - Subscription
    - Project Workflow
    - Order
    - User
    - Invoice
  governanceExamples:
    # Illustrative only. The LIVE authority for the subscription machine is
    # src/domain/billing/states.ts (SUBSCRIPTION_TRANSITIONS); transitionRules here
    # is documentation and is intentionally NOT wired into the runtime machine, to
    # avoid a second source of truth (see the s126-m01 single-source guard). Mirrors
    # the extend-8 set: terminated is terminal ([]) and pending_cancellation is
    # reversible back to active.
    - name: Subscription lifecycle
      transitionRules:
        future: [trialing, active, terminated]
        trialing: [active, terminated]
        active: [paused, pending_cancellation, past_due, terminated]
        paused: [active, terminated]
        pending_cancellation: [active, terminated]
        past_due: [active, unpaid, terminated]
        unpaid: [terminated]
        terminated: []
      requireTransitionReason: true
    - name: Content publishing
      transitionRules:
        draft: [review]
        review: [draft, published]
        published: [archived]
        archived: [draft]
      requireTransitionReason: false
  references:
    - "Trait Engine Spec v0.1 section 2"
    - "objects/provenance.v1.json#/references/ref-ca663a7c5731 — tone resolution for Colorized integration"
    - "objects/provenance.v1.json#/references/ref-be33a98177f7 — domain status mapping"
```
