Skip to content

actionObject

An action is a task in the dispatch Actions queue: something Helix would like an operator to do about a particular stop — rebook a job Spiral could not schedule, check whether a fault could be fixed remotely, look for voluntary work while a resource is still on its previous job.

Actions are Helix-only. They are id-managed, stored in Mongo and streamed to the screen, but are never sent to Spiral. An action is attached to its stop by the stop’s stable spiralKey rather than a server-assigned id, so it survives re-optimisation.

For what actions are for and how an operator works them, see the Actions guide. This page is the schema.

Authored per stop type at stopParams[<type>].helix.actions.<name>, resolved with clone inheritance and a DEFAULT base — so a definition on the DEFAULT key applies to every stop type. The key is the action’s name, which becomes type on the raised record.

{
"text": <string>,
"class": <string> | null,
"actionee": "any" | "owner" | "viewer" | "resource" | null,
"rank": <integer> | null,
"defer": <integer> | null,
"state": <string> | null,
"states": [string] | null,
"outcome": [string] | null,
"label": <string> | null,
"icon": <string> | null,
}
text string
THE RULE, and the message in one. A value ending in "()" names a function on the client crud module, called as a method on the stop — `this` IS the stop, so the rule reads this.state, this.arv, this.ext directly. Returning a non-empty string raises the action with that text; returning null (or anything that is not a string) does not raise, and cancels an automatic action already showing. A value without "()" is taken as literal text. An undefined function simply never raises.
class string
A name from the domain’s helix.actionClasses catalogue. Its presence is what makes an action AUTOMATIC — the auto-raise engine considers classed definitions only. Omit it for a manual message.
actionee "any" | "owner" | "viewer" | "resource" default any
Who sees it. resource: the resource fulfilling the stop (Engineer app only). owner: operators owning the stop’s desk. viewer: operators owning OR watching it. any: everyone. Enforced on the dispatch stream filter and narrowed again on screen.
rank integer
Arbitration for automatic actions: only the highest-ranked definition whose rule currently fires is kept in progress on a stop. Rank DOUBLES AS the raised record’s urgency (an unranked definition raises at 50, the middle of the scale) — one number, two jobs.
defer integer
Seconds. When set, the action offers a Defer outcome that hides ALL of this stop’s actions for that long. Note it hides rather than suppresses — see “Suppression” below.
state string
State gate: the action is eligible only while the stop is in this state, and is cancelled automatically when it leaves. Gates on the stop’s RAW Spiral state, not its clientState.
states string
As `state`, for several states.
outcome string
The decision buttons offered to the operator. Empty or omitted makes the action read-and-acknowledge. The chosen value is recorded to stop.actions.<name>.
label string
Display name for the queue row; defaults to the action name.
icon string
Icon for the queue row.
"helix": {
"actions": {
"rebook": {
"class": "planning",
"actionee": "owner",
"rank": 30,
"state": "REBK",
"label": "Rebook",
"text": "This job needs a new appointment — rebook with the customer.",
"outcome": ["rebooked", "cancelled"]
}
}
}

That definition needs no client code at all: the engine supplies the gate (state) and the suppression (the recorded outcome), so literal text is enough. Reach for a text function only when the rule depends on something a state cannot express.

What the engine writes to the action collection and streams to the screen. A domain never authors this directly; it is listed because it is what the Actions queue, the Engineer inbox and the Contact Centre all read.

{
"id": <integer>,
"spiralKey": <spiralKey>,
"type": <string>,
"urgency": <number>,
"class": <string> | null,
"actionee": "any" | "owner" | "viewer" | "resource" | null,
"rank": <integer> | null,
"defer": <integer> | null,
"text": <string> | null,
"outcome": [any] | null,
"createdAt": <integer> | null,
"stopType": <string> | null,
"did": <integer> | null,
"arv": <integer> | null,
"lockUntil": <integer> | null,
"seqno": <integer> | null,
"visibility": "resource" | "owner" | "viewer" | "all" | null,
"ext": <record> | null,
}
id integer
The action’s id.
spiralKey spiralKey
The stop this action is attached to — a stable logical address, not an outStopId, so it survives re-optimisation.
type string
The action NAME — the key it was raised from in the stop type’s helix.actions.
urgency number
0 (least) to 100 (most). Drives queue ranking and the temperature colour.
class string
Copied from the definition; null for a manual message.
actionee "any" | "owner" | "viewer" | "resource"
Copied from the definition.
rank integer
Copied from the definition.
defer integer
Copied from the definition.
text string
The resolved message — what the `text` expression returned at raise time.
outcome any
The choices offered. Null or empty means read-and-acknowledge.
createdAt integer
When it was raised (simulated time).
stopType string
Denormalised at raise time so an actions-only stream can resolve label, icon and class without loading the stop.
did integer
The stop’s desk at raise time. Denormalised — it may go stale when the plan changes.
arv integer
The stop’s planned arrival at raise time, as a sort key. Denormalised on the same terms as `did`.
lockUntil integer
Soft lock. While this is in the future the action is being handled elsewhere and shows greyed on other screens; it releases itself when the time passes. It is never a delete.
seqno integer
Optimistic-lock sequence number.
visibility "resource" | "owner" | "viewer" | "all"
DEPRECATED — superseded by `actionee`, and read only as a fallback when `actionee` is absent. Do not author it.
ext record
DEPRECATED — outcomes are recorded to stop.actions.<name>. Still the transport by which a chosen outcome reaches the recorder, so it is not yet removed.

Choosing an outcome forces it into the record’s ext.outcome, the recorder writes it to stop.actions.<name> and the audit trail, and then the action record is deleted — the queue entry is transient; the stop’s record and the audit are the durable trace. An external system concludes an action the same way: force ext.outcome, then delete. The other endings: defer (posted through the same door as {defer: seconds}, then deleted and later re-raised), cancellation (rule stops firing, state gate closes, the stop is deleted, or the definition vanishes in a domain reconfiguration — nothing recorded), and conclusion by a step — the callout’s answer window records a timeout refusal outcome when nobody answers in time, exactly as if it had been chosen.

The class field is the whole distinction, and it is worth being precise about because the name suggests something else:

  • Classed ⇒ automatic. The auto-raise engine considers only definitions carrying a class. It runs on every live stop write-back from the optimiser, keeps one action in progress per stop — the highest rank whose rule currently fires — and cancels it when the rule stops firing or the state gate closes. (One exception: the built-in callout chase is automatic by its own machinery and defaults CLASS-LESS — its class is pure routing, authored only when a domain wants it skill-matched.)
  • Class-less ⇒ manual. Raised by a person, not the engine: the two-way messageIn / messageOut facilities, and the action raised when a notification exhausts its delivery retries.

A class is also the skill gate: the queues show a classed action only to operators whose enabledClasses (set per user in the Administration app, picked from helix.actionClasses plus the built-ins) include it. Unset = every class; a class-less action reaches everyone. Class gates pools, not individuals — the resource audience ignores it (an engineer’s own inbox is addressed by identity). The filter is applied at the operator’s next sign-in.

An automatic action is re-evaluated on every stop write, so “handled” has to be expressed somewhere or it comes straight back:

  • Recording an outcome writes stop.actions.<name>. For a state-gated action that is enough — the engine will not re-raise it for the rest of that visit, and re-arms only if the stop re-enters the state.
  • For an ungated action the suppression is yours to author. The text function must consult this.actions[<name>] and return null once it is satisfied. Nothing else will stop it.
  • defer hides, it does not suppress. It sets a timestamp that makes the screen hide all of that stop’s actions for the period. The engine keeps the action raised throughout.
  • Actions guide — the role of actions, how operators work them, worked examples
  • Events guide — the other mechanism: timed, domain-authored change
  • stopParams — where helix.actions is authored
  • helixApp — the actionClasses catalogue
  • resourceParams — the standby callout’s synthesised action