eventObject
An event is a change that happens at a time, described declaratively. Where an action asks a person to do something, an event does something itself: move a stop into its next state, mark a resource off duty at a shift boundary, send the customer their promised update.
Events are re-derived from the stop’s current data on every change, never from a diff. That makes the model idempotent: an event that no longer applies simply stops being scheduled.
For the wider picture — which processes are events, which are actions, and how to add one — see the Events guide.
The four clauses
Section titled “The four clauses”Authored at stopParams[<type>].helix.event.simulator.<name>, resolved with clone inheritance over a
core foundation, so a domain can override a single clause of a built-in event and leave the rest.
Every clause is a Helix expression evaluated with this = { data, now, params } — this.data is the
stop, this.now the current simulated time, this.params the resolved config. Note this is not the
convention used by an action’s text, which binds this to the stop directly.
-
raiseexpression - Applicability, evaluated when the stop changes. Truthy schedules the event (replacing any instance already queued for this stop and name); falsy removes it. This is the gate that makes the model self-correcting — a stop that advances, is reassigned or completes drops its events without anything having to notice.
-
whenexpression - The absolute simulated epoch at which to fire, evaluated at the same moment as `raise`. Usually derived from the plan — the stop’s planned arrival or finish — rather than an offset from now, so the event tracks re-optimisation.
-
ifexpression - Re-checked at FIRE time against the freshly-read stop, and gates `then`. `raise` asked "should this be queued?"; `if` asks "is it still true now that we are here?" — the two are separate because the world moves between scheduling and firing.
-
thenexpression - The mutation. Returns a field-object which is committed against the stop as a force write. This is the only clause that changes anything.
The built-in simulator events
Section titled “The built-in simulator events”Core ships three events as the resolution foundation, so every stop type has them and a domain overrides only what it wants to change. Together they walk a stop through its lifecycle with no engineer present, which is what lets a scenario play out.
| Event | Transition | Timing |
|---|---|---|
headStop | DPLY → HEAD | a randomised acceptance delay after despatch |
arriveStop | HEAD/CMTD → ARVD | the planned arrival, with seeded jitter |
completeStop | ARVD → DONE | the planned finish, with seeded jitter |
"helix": { "event": { "simulator": { "arriveStop": { "when": "simAt(\"arv\")" } } }}The randomisation is deterministic — seeded from the stop’s id and state — so re-deriving an event does not re-roll its timing and a scenario replays identically.
Which events run live
Section titled “Which events run live”One engine, three surfaces, and they do not all run in the same conditions. This is the distinction to get right before designing a process around events:
| Surface | Live operation | Scenario replay |
|---|---|---|
helix.event.simulator.* — the stop lifecycle above | No | Yes |
Off-duty roster boundaries — STBY / REST entry and exit | Yes | Yes |
| The standby callout’s answer-window timeout | Yes | Yes |
The simulator exists to stand in for a real engineer, so it is deliberately confined to replay: in a live operation the states come from the field, and a simulator would be fighting reality. The roster and callout events are ordinary operational timing and run everywhere.
Related
Section titled “Related”- Events guide — what to use for which process, with worked examples
- actionObject — the other mechanism: asking an operator
- stopParams — where
helix.eventandonStateare authored - resourceParams — roster blocks and the standby callout