Supporting reference · Working design and evidence, not a promise of complete support.

On this page

Rails state transitions

Status: Implemented for one required, unconditional machine per Entity. Other machines keep storage and precise gaps.

A State Machine lowers to AASM over an explicit stored state Field. The current Compiler pins AASM 6.0.0 and emits workflow behavior only when an Entity owns exactly one required, unconditional machine with supported transitions. Every other machine keeps its storage column, initial-state default, and closed-domain validation, and the reviewed GapSet discloses the missing workflow behavior. Emitted AASM keeps its native scopes and forbids ordinary direct assignment. The State Machines owner (repository-only) states what states, events, guards, and transition effects mean.

On this page

Direction

Support: see the support inventory row (repository-only).

Use a plain closed value when no transitions exist. Generate modeled workflows with AASM over an explicit stored state Field. Oscar Party and Case Chat supply the current corpus proof on both database setups. Optional, broader-effect, and multiple-per-Entity machines retain storage and precise gaps. Framework-owned account status may still be realized by the selected authentication system rather than a second state machine. State initialization and validation explains why behavior-emitted columns use AASM initialization while storage-only columns retain their defaults.

Storage and behavior

State Machine Fields receive the generator-path storage partial. Storage-only required machines get a non-null string column and an initial-state default; optional machines get nullable storage without a default. Storage-only machines receive a closed-domain validation, with nil admitted only for optional machines. Each transition may be effect-free or assign one current_time or literal-null Value to an emitted optional mutable non-derived local datetime Field. Dynamic helper collisions select the deterministic Field namespace; fixed, class, or both-form collisions keep only storage. Every omitted applicability condition, transition, and effect is listed in the GapSet; admitted behavior suppresses only its corresponding records.

AASM machine

The implemented slice's State Machine contributor declares a complete immutable AASM 6.0.0 Gemfile block, direct requirement, empty prerequisite list, and source provenance to the one ApplicationTaskPlan-owned dependency plan. Core needs no projector; every nonempty selection is inserted once at Core's dependency marker, projected exactly once by the exact vendored Bundler 4.0.10, and closure-verified against the maximal universe without a production overlay lookup. The Compiler emits one named machine only for a required, unconditional State Machine on an Entity that owns no second machine. For that behavior-emitted machine it keeps native state scopes, forbids direct assignment, and admits the exact AASM 6.0.0 surface: fixed public and private instance helpers, fixed class helpers, state predicates and constants, all four generated methods per event, automatic state scopes, and the storage writer. Naming the AASM machine after its Field binds it to the authored storage explicitly. no_direct_assignment prevents ordinary model assignment from bypassing emitted events. A storage-only machine has neither that AASM control nor a Compiler substitute; its GapSet records the unrealized behavior. Admission compares that surface with the pinned Rails target methods and the emitted Field, Reference, Association, Predicate-scope, and Ordering-scope names. Private Validation methods allocate afterwards around AASM's chosen API. The enum allocator reserves the ordinary and namespaced state scopes. An authored query with the same name as an automatic state scope is declared later and retains its meaning; fixed AASM APIs keep their existing gap handling. On a realized Account Entity, the census also includes the fixed Account runtime members. A dynamic collision first selects AASM's Field-key namespace, preserving the existing member owner; a collision in both forms leaves behavior ungenerated. An admitted Ordering still owns its class name: an Ordering named aasm remains emitted while State behavior and its dependency are omitted. Generated model tests exercise the selected event and predicate names, allowed and rejected transitions, closed-state validation, effect assignment, and direct-assignment rejection after migration and schema loading. The Compiler's Oscar runtime qualification checks the AASM method census used by collision admission. Generated tests do not freeze private helpers, configuration, or the absence of AASM's bypass helper.

The intended lowering includes:

Each admitted effectful event assigns Time.current or nil in its transition callback before AASM's ordinary validated save. The transaction preserves persisted state and effects together, and exceptions propagate. Failed validation can leave an unsaved effect value; an application callback exception can also leave the attempted state in memory. The caller may intentionally reload or reset the object before retrying. Reload discards other unsaved changes. The Compiler adds no snapshot, restoration callback, automatic reload, or replacement persistence API.

AASM's explicit *_without_validation! helper remains available, including for effectful and namespaced events. That library escape hatch persists only the state column; timestamp effects and other dirty attributes stay unpersisted. The owner accepted that behavior for this conventional starting point. Ordinary bang events continue to save the state and effects together. One effect per transition is the current bound; authored ordering across several effects is not yet generated.

AASM callback source, its transaction wrapper, and its validation-bypass helper pin the inspected revision. The Case Chat runtime proves non-bang in-memory behavior, bang persistence, null clearing, invalid-transition stability, and database rollback. The qualification also exercises intentional reload and retry after a callback exception, initialization, and native state-only bypass on both database setups. Those are model/persistence boundaries; Scaffolds still emit no event request controls.

AASM is a current profile direction, not semantic Plan vocabulary and not a permanent ecosystem verdict. The source audits found that both AASM and state_machines provide guard-aware event enumeration. AASM won the historical generation comparison on conventionality, agent legibility, conservative compatibility, and the Compiler's ability to prevent event-name collisions. Statesman remains strongest when a persisted transition history is the central requirement, but it has no first-class event concept.

No transition-history or actor-audit shape is current. AASM has no built-in audit trail. Revisit history when an application requires "who moved this record, when, and with what metadata" as structured product meaning.

Automatic state scopes

The Compiler enables AASM's ordinary automatic scopes for both namespaced and unnamespaced machines. The native query API is a useful starting point even when the Plan does not explicitly request each method. Authored queries remain independently generated; concrete method collisions follow the allocation rules above.

AASM 6.0.0 has a known bug in namespaced scopes. For example, Conversation.status_active queries the stored value status_active, although the state is active. AASM also emits an unprefixed scope when that name is available. An emitted-model/PostgreSQL probe reproduced the prefixed bug; ordinary unnamespaced scopes returned the expected records. Issue #692 (repository-only) records the reproduction and upstream work.

The owner accepts this library limitation in a starting application. The user's agent can adjust an affected query if encountered. Enabling scopes does not depend on an upstream fix, and does not add a generated workaround, fork, or special namespace-based suppression. no_direct_assignment: true remains enabled. A scoped builder may raise even when assigning the initial state. Use ordinary creation followed by the named event where needed: constructing an active Case Chat Participant directly would skip accept! and its joined_at assignment.

State initialization and validation

For required state columns whose AASM behavior is emitted, the Compiler retains NOT NULL, omits the database default, and lets AASM assign the initial state. Its migration guidance explains that a database default can skip initial-state entry callbacks. AASM's own state validation replaces the extra generated presence or inclusion validator for that column.

An internally assigned nil can pass AASM's validator and raise ActiveRecord::NotNullViolation. For this internally managed value, that exposes an application bug rather than a form error the user can correct. The validation guidance explains the distinction.

This does not make NOT NULL a nonblank or state-membership check. AASM can accept an empty string, and the database constraint does not reject it. A PostgreSQL nonblank CHECK is explicitly deferred; revisit it when a concrete application write path establishes a need. No replacement callback, validation, or persistence wrapper is generated.

Storage-only state columns have neither AASM initialization nor AASM's membership validation. Their requiredness, default, and inclusion handling remains unchanged by this decision. The automatic-scope direction and retained direct-assignment option are described above.

References marked “repository-only” name implementation or internal material outside this public guide. They are intentionally not links. Publishing a design does not prove its implementation.