Skip to content

Assay

Assay at the trust boundary

When an entity arrives from outside, whether loaded from a store, deserialized off the wire, or rebuilt by a foreign system, you cannot trust that it actually belongs in the state it claims. Assay is the trust-boundary check: it runs a state’s declarative requirements (its guards and invariants) against an entity without firing a transition, answering “is this entity legally in this state?”

order := loadFromStore(id) // hydrated externally; claims to be Cooking
if err := machine.Assay(Cooking, order); err != nil {
return fmt.Errorf("order %s is not legally in Cooking: %w", id, err)
}
// Safe to resume from here.

By default Assay is fail-fast: it returns an *AssayError carrying the first requirement that failed. To collect every violation in one pass, useful for reporting or validation UIs, pass state.Aggregate():

err := machine.Assay(Cooking, order, state.Aggregate())
var assayErr *state.AssayError
if errors.As(err, &assayErr) {
for _, f := range assayErr.Failures {
log.Printf("violation: %s: %s", f.Name, f.Reason)
}
}

The error type is uniform across both modes; only how many failures it carries differs.

stateDiagram-v2
    [*] --> Hydrated
    Hydrated --> Assay: external entity
    Assay --> Resumed: requirements pass
    Assay --> Rejected: AssayError
    note right of Assay
        runs the state's guards/invariants,
        fires nothing
    end note

Use Assay wherever an entity crosses into your control before you resume driving it. It turns “I hope this object is valid” into a checked guarantee, without mutating the entity or advancing the machine.