Skip to content

Events

Helix has two ways to make something happen when the operation moves. They are easy to confuse, and picking the wrong one is the usual reason a process does not fire.

  • An action asks a person to do something. It lands in the Actions queue for a qualified operator to pick up — rebook this job, check whether the fault could be fixed remotely, look for voluntary work while the resource is still on its previous stop.
  • An event does something itself, at a time, with nobody watching. Move the stop to its next state, mark a resource off duty at the shift boundary, send the customer the update you promised them.

If a human has to decide, it is an action. If the system already knows what to do, it is a step — and an event is the timing machinery that fires it. In the process vocabulary, a process is composed of steps and actions; the event queue is how a step’s moment arrives.

One engine drives all of them, but they do not all run in the same conditions — which matters more than anything else on this page.

What it doesLiveReplay
The simulatorWalks stops through their lifecycle with no engineer presentNoYes
Roster boundariesSTBY / REST entry and exit at shift edgesYesYes
State-change hooksRuns your code when a stop changes stateYesYes

The reason a scenario plays at all. Three built-in events move each stop along — despatched to heading, heading to arrived, arrived to done — timed from Spiral’s own plan with deterministic jitter, so a replay is repeatable. A domain can override any single clause of them; the eventObject reference has the shape.

This is replay only, by design. In live operation the states come from the field, and a simulator would be arguing with reality.

Shift edges are ordinary operational timing, so they run everywhere. The off-duty periods on a resource’s head roster stop schedule themselves from the shift start, and fire whether or not a scenario is running. The standby callout’s answer window is the same machinery: if nobody answers within the contract term, the timeout fires and the call stands down.

State-change hooks — where your processes go

Section titled “State-change hooks — where your processes go”

The one to reach for when you want your process to run in a live operation. A stop type declares a matrix of transitions, and each cell names a function in your client module:

"onState": {
"ARVD": { "DONE": "tellCustomerComplete()" },
"*": { "REBK": "tellCustomerRebooking()" }
}

The keys are clientState values — the states your domain displays, not Spiral’s raw ones — and * matches any. The function runs with this bound to the stop, and receives the transition it fired on.

The worked example: tell the customer when their job is done.

Core ships the hook and the moment, never a sender. Helix tells you precisely when the operation reached the point you care about; what leaves the building is yours.

1. Say when, in the domain file. On the stop type the customer cares about:

"helix": {
"onState": {
"ARVD": { "DONE": "tellCustomerComplete()" }
}
}

The keys are clientState values and * matches any. Resolution is most-specific first: the exact cell, then the * row, then the * column.

2. Say what, in your client crud module. this is the stop, and the hook is handed the transition:

exports.tellCustomerComplete = async function (info) {
const to = this.ext?.customerPhone;
if (!to) return; // nothing to tell, nothing to do
await mySmsGateway.send(to, 'Your job is complete.');
};

A hook that throws is caught and never blocks the stop from being written — a failed message must not stall the plan. The corollary is that it also fails silently, so anything you need to know about has to be logged or retried by your own code.

Plugging a vendor into Helix’s own notifications

Section titled “Plugging a vendor into Helix’s own notifications”

Separately from your own sending, core has a notification queue with retry, delivery tracking and a token-authed inbound route for replies. It ships console, webhook and smtp adapters, which need no accounts. A client adds SMS, WhatsApp or voice by exporting a notifyChannels adapter map from its crud module — core then uses it for the notifications it raises, the standby callout above all.

That queue is currently core’s to use: a client registers channels for it and can override the callout’s own message hooks, but does not queue its own messages through it. For a process of your own, send from the onState hook as above.

When a notification on core’s queue exhausts its retries, Helix raises an action in the dispatcher’s queue. That is the two mechanisms working together as intended: the event tried to handle it automatically, and when it could not, it asked a person. A message you send yourself from a hook is outside that loop — raise your own signal if it matters.

  1. Decide which mechanism. Does a person need to judge something? Action. Does the system already know what to do? Event.
  2. Decide whether it must run live. If yes, it is an onState hook or a roster boundary — not a simulator event.
  3. Author the trigger declaratively where you can. A state-gated action needs no code at all; an onState cell needs one small function.
  4. Keep the function boring. Read what you need off the stop, do the one thing, return. These run inside the write path for every stop change, so they must be quick, and anything slow or unreliable belongs behind a queue of your own rather than inline.