Skip to content

helixAppObject

Application-level Helix behaviour — settings that apply to the whole client rather than to a single stop or resource type — is configured as direct children of the domain’s helix block, authored in the domain file as {"define": {"helix": { … }}} (a {"set": {"helix": … }} block is also accepted) and carried on the reference record. It is a Helix addition and is ignored by Spiral itself. (The pre-relaid helix.app.{…} nesting is still read as a legacy fallback, but new domains should use the direct form: helix.contactCentre, helix.heatmap, helix.metric, helix.actionClasses, helix.domains.)

contactCentre contactCentreObject
Contact-Centre (omni-search) configuration.
actionClasses string
Global catalogue of action class names. A class groups actions, marks a domain-authored definition AUTOMATIC, and is the SKILL GATE: operators are enabled per class in the Administration app (the picker offers this catalogue plus core's built-ins), and the queues show classed actions only to enabled operators. Class-less reaches everyone; the resource audience ignores class. See actionObject and builtInActions.
heatmap { radius, idle, late, unresourced }
Zoomed-out heat-map presentation: radius (metres, default 7500 — each heat point draws ~2×radius across) and the three signal colours as Helix palette names (or CSS values): idle (default "blue"), late ("red"), unresourced ("purple").
metric { [name]: { median, var, low, high } }
Metrics-panel tile colour tolerances, keyed by metric name (sla, stopsPerHour, driveTime, unresourced). A value ≤ median−var shows the `low` colour, ≥ median+var the `high` colour, in between amber — direction is encoded by the colours (e.g. sla: {"median":70,"var":20,"low":"red","high":"green"}; unresourced would reverse them). Colours are Helix palette names or CSS values; unconfigured metrics keep the built-in colouring.
domains { [domain]: { [skill]: "skill" | "constraint" | "capacity" } }
The domain master list (see below). Groups skills by domain and declares each skill’s value-form; the domain names are also the surge domains referenced by surgeZone and the per-type `domain` blocks.
dispatch { resourceList: { sort }, projectList: { sort } }
Dispatch-application configuration: named sort rules for the resource list and the project list. See below.
hdr { [entity]: string[] }
Declared header fields per entity — the unwrapped first panel of the editor. Each name binds an input to ext.<field> (labels via vocab). Core defaults resource and project to ["name","tel"], responsePoint to ["name"]; override per entity, or [] to switch the header off. See below.
archive archiveObject
The archive framework: moving completed records and older audit out to a backing store. OFF unless this block is present (null/absent = off). See below.
field { arrivalGate }
Field-device (engineer app) shell config the web serves to the native shell — tuned by a domain redeploy, never a mobile-app store release. Today: the getLoc arrival-fix gate. Absent → core defaults. See below.
addr { name, addr, at, tel, area }
The doors that expand an entity's addr record: short name, full address, coordinate, telephone and area. See below.
fed { block }
How this node behaves when it is FED — driven from standard input by a host application. Today: the entities the console and API may not write while the feed is the author. Ignored on every other kind of node. See below.
hub { nameField }
Which field inside the client-proprietary addr object is the hub's display NAME, used for list rows and map markers. Unconfigured, the display falls back to the stringified addr.
tests { reliableRuns }
Regression library: how many consecutive passes make a test reliable enough to be proposed for promotion. Absent → core's default.

helix.dispatch configures the dispatch application’s lists. Both the resource list and the project list take named sort rules, and both work the same way:

"dispatch": {
"resourceList": { "sort": { "Activity": ["PACK","FREE","BRK","TIDY","WRAP","RECY","HEAD"] },
"signedOffDueWithin": 43200 },
"projectList": { "sort": { "Urgency": ["OSRC","REBK","PLAN","HEAD","ARVD"],
"Value": "byContractValue()" } }
}

resourceList.signedOffDueWithin is the one other setting here: how far ahead, in seconds, a resting resource’s next shift may start before the resource list’s Hide signed off option stops hiding it. Absent, it is 12 hours (43200). It changes only what that option hides — a resource whose shift start has already passed is never hidden by it, at any window.

Each key becomes an extra entry in that list’s Sort menu, alongside the built-in State, Delay and Earliest. State is always offered first and is the default: it orders on Helix’s own state vocabulary — the raw stop state (and, for resources, the roster duty type) — so it cannot fall out of order when a domain renames a state. Your named rules are the opt-in layer on top, for ordering the vocabulary you invented.

A rule takes one of two forms.

An array — a sequence. The rows rank by the position of their display status (clientState) in the array; anything unlisted sorts after everything named. This covers the common case without writing code.

A string — an order expression. A domain expression bound to a function in your client module, called once per comparison with the row as this and the row it is being compared against as its first argument. Return true when this sorts before that row, false when it sorts after:

// in the client module
byContractValue(other) {
return (this.data.ext?.value ?? 0) > (other.data.ext?.value ?? 0);
}

Any arguments written into the expression itself follow the compared row, so "byField('earliest')" arrives as byField(other, 'earliest').

Use the expression form when the ordering depends on something a list of status names cannot express — a number, a date, a computed score. Helix asks the expression both ways round for each pair, because a “sorts before” test alone cannot distinguish after from equal: rows your expression ranks equally then fall through to the built-in tie-breakers (delay, then earliest / planned arrival). An expression naming a function no module defines leaves the order to those tie-breakers rather than scrambling the list.

helix.domains is the client’s domain master list: it groups skills under named domains and declares each skill’s value-form. The domain names form one shared namespace used three ways:

  1. Skills grouping (here). Each skill is typed skill, constraint, or capacity; the type fixes the value the skills editor writes — skill → null (a boolean capability), constraint → { "max": n }, capacity → { "capacity": n }.
  2. A type’s default domain — helix.domain on a stop / resource / project type names the group the skills editor opens on for that type (Helix-only; not sent to Spiral).
  3. Surge domains — the same names label the per-type surge domain blocks and the surgeZone domain field.
{"define": {"helix": {
"domains": {
"Police": { "police":"skill", "firearms":"skill", "publicOrder":"skill" },
"Ambulance": { "paramedic":"skill", "convey":"skill", "stretcher":"capacity" },
"Fire": { "fire":"skill" },
"General": { "seats":"capacity" }
}
}}}

helix.domains is opt-in: a client that declares none keeps the free-form skills editor. Any skill referenced by a type (in coreSkills, initialSkills, exclusiveSkills, secondaryActivity.skills, or a stop option) that is not in the master list is flagged with a warning at domain-seed time — the seed still completes.

An entity’s addr object is the loc within the helix objects — its general location record. The schema is proprietary to the client, so nothing reads its fields directly: four domain-overridable doors expand it, each an expression resolved against the app-level helix.addr block:

{
"name": <string> | null,
"addr": <string> | null,
"at": <string> | null,
"tel": <string> | null,
"area": <string> | null,
}
name string
Expression for the SHORT NAME. Default helixAddrName() — the addr's `name` field.
addr string
Expression for the FULL ADDRESS. Default helixAddrText() — the non-empty of name, line1, line2, area, postcode, joined in that order.
at string
Expression for the COORDINATE. Default helixAddrAt() — the addr's `at` field ([lat,lng]).
tel string
Expression for the TELEPHONE. Default helixAddrTel() — the addr's `tel` field.
area string
Expression for the AREA summary. Default helixAddrArea() — the addr's `area` field. The addr subsumes the bare Location `area`: a location is `{at, addr}` and the area is pulled from the addr (the legacy `{at, area}` shape keeps working as a fallback until authoring migrates).

The default structure is { name, line1, line2, area, postcode, at, tel }. A client whose records differ (the AA pack authors {name, lines, area, code}) overrides the doors — author the block and implement the functions in the client extension (the SDK exports the defaults to compose with):

{"define": {"helix": {
"addr": { "name": "myAddrName()", "addr": "myAddrText()" }
}}}

Everything that displays an address resolves through these doors — hub names on the map and lists, the stop editor’s hub schedule row (the staged leg’s hid resolved to its hub record), and its hover/full address. addr itself is Helix-only: it is never sent to Spiral, which speaks ids and coordinates.

Configures the Contact-Centre contract search — the omni-search box operators use to find a contract or customer record and open or attach work to it.

{
"searchPaths": [string],
}
searchPaths string
Dot-paths into a contract record that the local search matches against (e.g. "fields.membershipNo", "fields.reg", "customer"). Drives the built-in /api/contract/search query.

The client may also supply a searchContracts(input) resolver (in its clientConfig) to merge results from an external system — CRM, telephony or ticketing — with the local matches. See Develop a client extension.

helix.hdr gives each entity editor a header area — the first panel, rendered without section chrome — from the domain alone, no client code. Name the ext fields to show:

"hdr": { "resource": ["name", "tel"], "project": ["name", "tel"] }

Each field becomes an input bound to ext.<field>; its label comes from vocab() (so "tel" can read “Mobile” via helix.vocabulary). The core already defaults resource, project to ["name", "tel"] (responsePoint to ["name"] — a coverage point has no phone), so a name/phone header appears with no config; author an entry to change the fields, or [] to remove the header. A client that needs a richer header (a lookup, validation) registers a …HdrExtension component instead (see Develop a client). name/tel are read back by the core getName()/getTel() doors.

helix.archive turns on the archive framework — moving completed records and their audit out to a durable store, keeping Helix and the optimiser to live work. It is off unless this block is present; null or absence leaves everything live. It never runs on a simulated (scenario) node.

"archive": {
"cadence": "00:05:00",
"completedDelay": "01:00:00",
"partialAudit": { "maxEntries": 5000, "maxAge": "7#00:00:00" },
"entities": { "responsePoint": { "enabled": true } }
}
{
"enabled": <boolean> | null,
"cadence": <duration> | null,
"batch": <number> | null,
"auditRows": <number> | null,
"dbName": <string> | null,
"completedDelay": <duration> | null,
"partialAudit": { maxEntries, maxAge } | null,
"entities": { [entity]: { enabled?, fn?, delay? } } | null,
}
enabled boolean
Pause switch for a present block (default true). The usual off switch is to omit the block or set helix.archive: null.
cadence duration
How often a sweep runs (seconds, or "D#HH:MM:SS"). Default 60s.
batch number
Max completed entities archived per sweep (default 25).
auditRows number
Max audit rows moved per sweep across all drains (default 2000).
dbName string
Override the built-in sink database name (default HELIX_ARCHIVE_DB / "helixArchive").
completedDelay duration
Grace after an entity completes before it is archived — the window operators can still see finished work (default 1h).
partialAudit { maxEntries, maxAge }
Bounds the live trail of still-live entities: drain a record's oldest audit past maxEntries, and any entry older than maxAge. A resource never drains past its most recent off-duty point (a shift is never split).
entities { [entity]: { enabled?, fn?, delay? } }
Per-class knobs for entities without a Params catalog — e.g. responsePoint (detection ships but archive is off until enabled) and notification (the reaper delay). Per-TYPE control uses <entity>Params[type].helix.archive instead.

The destination per entity type is <entity>Params[type].helix.archive: omit it for the built-in sink, null for accept-and-discard (still deleted), or a client function name. See the audit trail for the model and Develop a client for the function contract.

helix.field is the config the field device (the engineer’s app) reads from the origin and passes to its native shell. Because it arrives with the domain rather than being baked into the mobile app, tuning it takes a domain redeploy, never an app-store release. Today it carries the arrival gate: the thresholds the shell verifies a location fix against when an engineer taps arrived, before that fix is paired as the resource’s position.

{
"arrivalGate": { maxAccuracyM, maxAgeS, minMoveM, maxSpeedMps? } | null,
}
arrivalGate { maxAccuracyM, maxAgeS, minMoveM, maxSpeedMps? }
Thresholds for the one-shot arrival fix. maxAccuracyM: reject a fix worse than this horizontal accuracy in metres (default 200). maxAgeS: reject a fix older than this many seconds, measured on the platform monotonic clock (default 60). minMoveM: suppress a fix within this distance of the last answer — 0 for a single tap, which is the default. maxSpeedMps (optional): suppress a fix implying an implausible speed from the last answer. Each field overrides the core default independently.
"field": {
"arrivalGate": { "maxAccuracyM": 100, "maxAgeS": 30 }
}

Only the fields you name change; the rest keep their core defaults. A fix that fails the gate — or a device with no shell (a plain browser) — simply sends the arrival without a location, and Spiral’s own stop-location default stands. See Events for the device-side mechanism this feeds.

helix.fed is how the client configures a node that is fed — the spiral mode, where the whole Helix stack runs but its command stream arrives on standard input from a host application rather than from people and scripts. The host owns the stream; Helix persists, serves and displays what passes through it. On every other kind of node this block is ignored.

"fed": { "block": ["resource", "project", "desk", "responsePoint", "surgeZone"] }
{
"block": [string] | null,
}
block string
The entity types the console and the /json API may NOT write while the feed is the author: resource, desk, project, responsePoint, surgeZone, hub, user, system, action. Anything not named is writable. Absent or [] blocks nothing. An unrecognised name is a FATAL domain error — a typo here would otherwise block nothing and say nothing.

It is a block list, and the empty case is permissive. A fed node that finds no list writes a line at startup saying so, and naming every entity its console can therefore write — the default is not a thing to discover from behaviour.

What belongs on it. The host’s own entities: whatever its optimiser plans and it expects to remain the only author of. What is left off is what Helix owns and the host has no opinion about — user and system never reach Spiral at all, and a deployment may hand over more over time. That is the point of a list rather than a switch: it is the migration dial. Each function that moves off the host onto Helix takes its entity off the list, and nothing else changes.

A blocked write is answered 403 naming the entity, and the operator’s editor for it opens read-only, so the refusal is visible before anyone types into it rather than after. The rest of the console — the map, the lists, the audit trail, Mission Control — is unaffected.

The schedule is not on the dial. The list refuses authoring — add, update and delete of a blocked entity’s own attributes. A force or coerce is a placement: where a stop goes, which resource takes it, whether it is locked. Spiral accepts a placement from any feeder and answers it on the stream the host reads, tagged 0, so the host can tell it from its own. Placements therefore pass whatever the list says: dragging a stop onto a resource or a desk, Auto Allocate, and advancing a stop’s state all work on a fed node, while the editors for the blocked entities stay read-only. The rule lives in one place, shared/fedPolicy.ts, and both the console’s guard and the API’s refusal ask it.

Two rules hold on a fed node whatever the list says, because they are what makes the mode a drop-in:

  • Helix authors nothing of its own. Its housekeeping — roster convergence, archive deletes, notification outcomes — is suppressed and logged, so the same input produces the same result whether it is piped through Helix or straight into the optimiser.
  • A write asking to be tagged is tagged 0. Tag numbers belong to whoever owns the feed, and tag 0 is the agreed marker for “this transaction is not yours” — it lets the host recognise a change that arrived beside its stream, and ignore it. See Access from outside the SPA.