Transition constraints

A transition constraint declares one permitted step of a state machine over your data — for example, that an order's status may move from pending to processing. Once any constraint watches a property, the server enforces the state machine on every write: a change nobody declared is rejected with 422 Unprocessable Entity, and nothing in the document changes.

Constraints are stored as HTML Microdata items, in any document on the host, and discovered automatically — the same way triggers are. Discovery is inheritance-aware: if a schema on your host declares a type with parent set to https://pagelove.org/TransitionConstraint, items of that subtype are enforced exactly like plain constraints.

Constraints validate writes. To be notified when a permitted change commits, pair them with a transition handler — the two are independent, and neither requires the other. For a worked end-to-end example, see Declaring a state machine.

Quick example

One step of an order lifecycle — status may move from pending to processing:

<div itemscope itemtype="https://pagelove.org/TransitionConstraint">
  <meta itemprop="selector" content="[itemtype='https://example.com/Order']">
  <meta itemprop="property" content="status">
  <meta itemprop="from" content="pending">
  <meta itemprop="to" content="processing">
</div>

Properties

selector

Required. A CSS selector naming which items the rule watches. Pagelove selector extensions are allowed — :isa('https://example.com/Order') matches the type and any schema-declared subtype.

property

Required. The Microdata property that holds the state.

from and to

At least one must be present. A rule with both permits one step: the property may change from the from value to the to value.

Omitting one end declares a lifecycle boundary:

A constraint needs at least one of from and to; one that declares neither is ignored and never enforces anything (on hosts running the platform schemas, writing such a rule is rejected with 422). The empty string "" is an ordinary state value, distinct from absent: content="" permits transitions involving the literal empty-string state, and does not declare an entry or exit.

Strictness

Declaring even one constraint on a property makes the server strict about that property on matching items. Every appearance, change, and disappearance of the watched property must then match a declared rule; anything undeclared is rejected with 422. A write that does not change the watched value is not a transition and always passes.

Strictness is scoped by the constraint's own selector and property:

Strictness closes both ends of the lifecycle:

The exit rule has a consequence worth planning for: a fully constrained item cannot be deleted until some rule declares its exit. Without one, a DELETE of the document holding the item is rejected with 422. This is deliberate — otherwise a client could delete and recreate an item to skip the state machine. Declare an exit from every terminal state. If you forgot and the data is now stuck, see the WebDAV repair path.

The watched property must be single-valued

A state property must have exactly one value on an item — an item carrying several values for it has no well-defined state. A write that would produce that shape on a constrained item is rejected with 422, naming the item and property.

Item identity and @key

To validate a whole-document write, the server compares the stored document with the incoming one, and must decide which old item is which new item. Pairing uses the schema's primary key: a property annotated @key (which must also be individually unique). Two items of the same type are the same item when their key values match.

Three rules follow:

The fix for the ambiguous case is to declare a @key property in the type's schema, so every item carries a stable identity between writes. Selector-scoped writes name the exact element they change, so identity is inherent there — the pairing rules matter for whole-document writes.

The 422 body

A rejected transition returns 422 Unprocessable Entity with an HTML Microdata body in the platform's ConstraintViolation vocabulary — the same shape as uniqueness violations, with failedConstraint of the form transition(<property>):

<body itemscope itemtype="https://pagelove.org/ConstraintViolation">
  <h1 itemprop="name">Unprocessable Entity</h1>
  <meta itemprop="statusCode" content="422">
  <p itemprop="description">Transition constraints violated</p>
  <ul>
    <li itemprop="violations" itemscope itemtype="https://pagelove.org/Violation">
      <span itemprop="constraintSelector">[itemprop='status']</span>
      <span itemprop="failedConstraint">transition(status)</span>
      <span itemprop="message">Transition violation: 'https://example.com/Order' item 'SKU-B'
        property 'status' may not change from 'pending' to 'shipped'
        (constraint declared in '/transitions/rules.html')</span>
      <span itemprop="itemtype">https://example.com/Order</span>
      <span itemprop="property">status</span>
      <span itemprop="key">SKU-B</span>
      <span itemprop="from">pending</span>
      <span itemprop="to">shipped</span>
    </li>
  </ul>
</body>

Parse it with a microdata parser and read each violations item's itemtype, property, from, and to to drive your UI:

Concurrent writes

If two clients race to perform the same step — both trying to move the same order from pending to processing — exactly one wins. The loser's write is refused with 412 Precondition Failed rather than applied against stale state. On a 412, re-read the document and retry: the rules re-check against the current state, so a step that has already been taken then fails with 422 because the source state has moved on.

One consequence for selector-scoped writes: on a host with transition rules, an unconditional selector write touching watched items can return 412 where it would otherwise have been applied silently over a concurrent change. Handle 412 by re-reading and retrying, the same as a conditional write.

Taking effect

A constraint binds as soon as the document holding it is written through the serving path — the very next write validates against the new rules. Rules edited over WebDAV instead take effect within 60 seconds.

A stored rule missing its selector or property is ignored; a malformed rule never blocks writes.

WebDAV bypasses constraints

WebDAV authoring writes are never transition-validated. The authoring tier edits documents raw, by design — it is how humans edit content directly, and the escape hatch for repairing data that a state machine has wedged. See the repair path.

See also