Actions
An action is a flag raised against a stop that asks an operator to look at it and make a decision. The system raises the flag, an operator processes it in the context of the job, and the outcome is recorded and audited. Actions are how you put a human checkpoint into an otherwise automatic plan — most usefully, to stop the wrong resource being sent, especially where the cost to serve is high.
(For the screen itself see the Introduction; for how domain configuration fits together see the Helix technical overview.)
Why actions exist
Section titled “Why actions exist”Spiral optimises the whole operation against your model of cost. Most of the time the plan it builds is the one you want. But the lowest modelled cost is not always the right answer:
- a long deadhead to reach a job when a nearer resource was almost as good,
- a premium or scarce resource committed to routine work,
- a plan that technically satisfies a contract but breaches its spirit,
- a high-value job about to be served by a resource you would rather hold back.
You cannot economically encode every one of these as a hard constraint — and you would not want to, because the right call often needs a human. An action lets you encode the trigger — “raise a flag when the plan looks like this” — and leave the decision to a controller. The plan keeps moving; the flag waits for a person where a person adds value.
How operators use actions
Section titled “How operators use actions”Pending actions appear in the Actions list in the dispatch screen’s left column. They are ordered with the hottest first — by urgency, then by the stop’s arrival time, latest first — so the most pressing checks sit at the top.
- Click an action and Helix opens the relevant job — its resource, project or response point — in the editor, with the action shown at the top. You always process an action in the context of the root entity, so you can see the plan you are about to change.
- The action also appears whenever you open that editor yourself on a stop that has one pending — you do not have to come in through the queue to deal with it.
- Process it by choosing an outcome button (which records your decision), or Defer it (snooze the stop’s actions for a while), or Acknowledge it when it is just a message. While you have an action open it is soft-locked, so it greys out on other controllers’ screens.
Who sees which actions
Section titled “Who sees which actions”Two settings on each action decide who it reaches:
-
Class — an action with a class is only shown to operators who have enabled that class. This lets a team divide responsibility: tyre sourcing to one desk, recovery exceptions to another. An action with no class is a simple message that anyone can service.
-
Actionee — the audience for the action:
Actionee Reaches anyeveryone ownerthe dispatch desk that owns the stop viewerany desk owning or watching the stop resourcethe resource fulfilling the stop — i.e. the Engineer view
Use owner for decisions that belong to whoever is managing the work (correcting the plan), and resource
for things the person doing the job should see.
Two kinds of action
Section titled “Two kinds of action”- Automatic (classed) — raised by the system from a rule. The backend evaluates the rule every time the plan changes and keeps one automatic action in progress per stop (the highest-ranked one whose rule currently fires). Classed actions are the ones you use to police the plan.
- Manual (no class) — raised by an operator from the editor. Everyone can see them; nothing is automatic.
Configuring actions
Section titled “Configuring actions”Actions are defined per stop type, in the domain file, under helix.actions on the stop’s parameters. Like
the rest of stopParams, they are inherited through clone, so a base type can carry an action that all
its descendants share. The available class names are listed once, globally, in helix.actionClasses.
// reference helix block — the catalogue of class names"helix": { "actionClasses": ["recovery", "resourceBehaviour", "tyres"]}// stopParams[type] — actions attached to one stop type"helix": { "actions": { "tyreSource": { "class": "tyres", // only operators who enabled "tyres" see it "actionee": "any", "rank": 20, "defer": 300, // a Defer outcome snoozes this stop's actions for 5 min "text": "sourceTyre()", // expression → message string, or null to not raise "outcome": ["success", "fail"] } }}The action definition
Section titled “The action definition”| Field | Meaning |
|---|---|
text | The rule, in the client-extension convention. A value ending in () names a domain function called as a method on the stop — this is the stop, so the rule reads this.arv, this.ext, … and whatever the plan exposes — returning the message the operator sees, or null/non-string to not raise (an automatic action already showing is then cancelled). A value without () is taken as literal text. A function that isn’t defined simply doesn’t raise, so a flag only appears once its rule exists. This is where the “when the plan looks like this” logic lives. |
class | A name from helix.actionClasses, or omit for a class-less message. A class makes the action automatic and gates it to operators who enabled that class. |
actionee | The audience — any / owner / viewer / resource (see the table above). |
rank | Priority. For automatic actions it also decides which one wins: only the highest-ranked action whose rule currently fires is kept on a stop. It drives the urgency/temperature colour too. |
defer | Seconds. If set, the action offers a Defer button that pauses all of the stop’s actions for that long — useful when you have acted off-system and don’t want the flag back immediately. |
outcome | The decision buttons. An array of choices; empty/omitted means the action is read-and-acknowledge only. |
label, icon | Optional display name and map/list icon for the queue row. |
Two-way messaging
Section titled “Two-way messaging”messageIn and messageOut use the same action format (actionee, urgency, and a text gate) and live
in the stop defaults, so they’re configurable per stop type. Here text is reused as an enable gate:
true = always available, an expression = available per stop (e.g. only in certain states), and an absent
messageOut hides the facility entirely. messageOut is the dispatch → resource direction — the Message
resource button in the stop editor; messageIn is the resource → dispatch direction (the engineer view). By
default the resource’s NOW stop is messageable both ways (text: true, actionee resource/owner);
other stops carry no messageOut, so the message panel stays hidden until you add one.
Worked example: don’t send the wrong resource where cost to serve is high
Section titled “Worked example: don’t send the wrong resource where cost to serve is high”The goal is to flag a stop before it is committed when the plan would serve it with a resource that costs more than you are willing to pay — so a controller can reassign, or consciously accept it.
// stopParams for a high-value job type"helix": { "actions": { "costToServe": { "class": "resourceBehaviour", // the desk responsible for resource economics enables this "actionee": "owner", // it is the owning desk's decision to correct "rank": 60, // outranks routine flags, so it wins the slot "defer": 600, "label": "Cost to serve", "icon": "Warning", "text": "highCostToServe()", // your clientModule rule — below "outcome": ["reassign", "accept", "escalate"] } }}// crudClientModule — the client's backend domain functions, loaded at runtime. `this` is the stop being// scheduled, so the rule reads whatever the plan exposes on it (the planned resource and its modelled cost// are domain-specific fields).highCostToServe() { const planned = this.plannedServeCost; // what the current plan will cost to serve this stop const target = this.targetServeCost; // the most you are willing to pay for it if (planned == null || target == null) return null; // can't judge → don't raise const over = planned - target; if (over <= 0) return null; // within target → no flag return `£${Math.round(over)} over target — review the planned resource`;}What happens:
- Each time Spiral re-plans, Helix evaluates
highCostToServe()against the stop. While the planned cost is within target it returnsnulland nothing shows. The moment the plan would overspend, it returns the message and the Cost to serve action is raised on the owning desk. - Because it is ranked
60, it takes the stop’s automatic slot ahead of lower-ranked flags (a routinewrongSkillwarning at rank20, say). One stop never nags with two automatic actions at once. - The controller clicks it, lands in the job with the plan in front of them, and chooses an outcome — reassign (and correct the plan), accept (record that the spend is justified), or escalate.
- The outcome is recorded on the stop and audited, which suppresses the flag from coming straight back for the same situation. If they instead Defer, the stop’s actions go quiet for ten minutes while they sort it out off-system.
Tune the threshold in the rule, not the UI — the same pattern covers “resource too far”, “premium vehicle on
routine work”, or “holding a resource back”: each is a clientModule function that returns a message when the
plan crosses your line, and null when it doesn’t.
What gets recorded
Section titled “What gets recorded”Processing an action writes its outcome to the stop (stop.actions.<name>) and into the audit trail, then
clears the flag. That record is what stops an automatic action re-raising for a situation you have already
ruled on. A Defer outcome instead timestamps the stop so its actions stay paused until the deferral
passes. Either way the plan continues to optimise underneath — actions sit alongside it, they don’t freeze it.