Skip to content

Replace the onEnter event-name convention with first-class state entry hooks #54

Description

@camcima

Finding #6 of the 2026-08 architecture review, carried over from the June 2026 review's design note. Deferred from #52.

Problem

OnEnterObserver implements "run something when entering state X" as a magic event name layered on ordinary transition events. To give approved an entry hook you must declare a sentinel self-transition (docs/observers.md):

.addTransition("approved", "approved", { event: "onEnter" })

That has real costs:

  • Graph pollution. Sentinel self-loops appear in GraphBuilder output as genuine edges, so every DOT/Mermaid export of a workflow with entry hooks is wrong as documentation — it shows transitions the domain does not have.
  • Name collision. A workflow with a real domain event called onEnter silently becomes an entry hook (or vice versa). The convention has no namespace.
  • It leans on a subtle semantic. The mechanism works because event-driven self-transitions fire event observers without moving state — intentional and tested, but non-obvious, and it means entry hooks ride on an edge case rather than on a first-class concept.
  • Ordering is implicit. The hook is enqueued as a separate top-level operation, so "on entry" really means "some time after the operation that entered it drains". The ifStateName guard added in v4 fixed the wrong-state bug, but the timing is still surprising for anyone reading onEnter as a lifecycle hook.

Proposed direction

Declare entry hooks at build time as their own concept, keyed on the state rather than on an event name:

new ProcessBuilder("order")
  .addState("approved", { onEnter: [chargeCard] })
  // or: .addOnEnter("approved", chargeCard)

A generic after-observer then dispatches on frame.toState, with no sentinel edge in the graph and no event-name namespace to collide with. Decide explicitly, and document, whether hooks run inline within the committing operation or as a chained operation as they do today — the current asynchronous behavior is defensible, but it should be a stated contract rather than a side effect of the implementation.

OnEnterObserver can remain as a deprecated shim for one major if that eases migration.

Acceptance criteria

  • Entry hooks are declarable on a state without a sentinel transition.
  • Graph exports contain no artificial self-loops for states with entry hooks.
  • An event genuinely named onEnter no longer behaves differently from any other event.
  • Execution timing (inline vs chained) is documented as a contract and pinned by tests.
  • Migration guide entry under docs/migration/.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions