scrml.dev v0.7.1
Reference › Elements

<onTransition>

Effect on transition. Runs when an engine variable moves from one variant to another. Filterable by from / to / if / once.

Syntax

<onTransition [from=.Variant] [to=.Variant] [once] [if=cond]>
  <!-- effect body -->
</onTransition>

All filter attributes are optional individually, but at least one of from= or to= is required — without a target, E-ONTRANSITION-NO-TARGET fires.

Worked example

A loading flow with analytics on the success transition and cleanup on the error transition.

type Phase:enum = { Idle, Loading, Error(msg: string), Success(count: int) }

<engine for=Phase initial=.Idle>
  <Idle    rule=.Loading />
  <Loading rule=(.Success | .Error) />
  <Error   rule=.Loading />
  <Success rule=.Idle />

  <onTransition from=.Loading to=.Success>
    ${ analytics.track("load.success") }
  </onTransition>

  <onTransition to=.Error>
    ${ cleanup() }
  </onTransition>

  <onTransition from=.Idle to=.Loading once>
    ${ firstLoadInstrumentation() }
  </onTransition>
</engine>

Three handlers, three filter shapes: from-and-to (specific edge), to-only (any incoming Error), and from-and-to-once (first occurrence only).

Semantics

  • Fires after the transition commits. The engine variable has already taken the new variant when the handler runs. Reads of the engine var inside the body see the destination state.
  • Lives inside an engine. <onTransition> is a child of <engine>, not free-standing. It is also forbidden inside <match> blocks — E-MATCH-ONTRANSITION-FORBIDDEN. Match is pass-through case analysis; effects belong on engines.
  • from= / to= are filters. Omitting one means the handler fires for any source / destination of the other. Omitting both is the E-ONTRANSITION-NO-TARGET error.
  • once is a bare attribute. Marks the handler to fire only on the first matching transition. The compiler tracks a per-handler "has- fired" flag.
  • if= is a runtime guard. Expression evaluated at transition time; handler fires only when truthy. Composes with from / to / once.
  • Multiple handlers per engine are allowed. The engine can have any number of <onTransition> children with overlapping filters. All matching handlers fire in source order.

Errors this feature can fire

Edge cases

  • Self-writes don't trigger the handler. Idempotent self-writes (writing the current variant) are runtime no-ops per W-ENGINE-SELF-WRITE-DETECTED; no transition occurs so no handler fires.
  • internal:rule= transitions skip the handler. Inside a composite state-child with internal:rule=, the inner-engine transition preserves the outer state — the outer engine's <onTransition> does NOT fire. Per SPEC §51.0.O.
  • effect= on a state-child is the per-rule alternative. For single-target rules, an inline effect= attribute is the shorter form. Multi-target rules fire E-ENGINE-EFFECT-AMBIGUOUS because the destination is ambiguous; use <onTransition> instead.
  • Once + if= compose. once if=cond fires only on the FIRST transition where cond is truthy. Subsequent matching transitions are skipped whether cond is truthy or not.

Related features

  • <engine> — the container. Without an engine, onTransition has nowhere to live.
  • effect= attribute on a state-child — per-rule inline form. Single-target only. Reference page queued.
  • <onTimeout> / <onIdle> — sibling temporal triggers (per-state and engine-wide watchdog respectively). Reference pages queued.
  • <match> — forbidden context. Promote to engine first if you need transition effects.

Availability

Surface Since Notes
<onTransition> from / to / once / if S57 D2.8 SPEC §51.0.H; engine-only structural element
E-ONTRANSITION-NO-TARGET S74 A1b B17.3 Caught neither-from-nor-to misuse
internal:rule= excludes onTransition S67 / S68 §51.0.O Internal transitions preserve outer-engine lifecycle including handler skip

Specification

This page summarizes SPEC §51.0.H (effect= / onTransition) and §51.0.O (internal:rule= prefix). The normative text lives at compiler/SPEC.md in the scrmlTS repository.