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.)
-
contactCentrecontactCentreObject - Contact-Centre (omni-search) configuration.
-
actionClassesstring - 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.
-
archivearchiveObject - 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
Section titled “helix.dispatch”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 modulebyContractValue(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
Section titled “helix.domains”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:
- Skills grouping (here). Each skill is typed
skill,constraint, orcapacity; the type fixes the value the skills editor writes —skill→null(a boolean capability),constraint→{ "max": n },capacity→{ "capacity": n }. - A type’s default domain —
helix.domainon a stop / resource / project type names the group the skills editor opens on for that type (Helix-only; not sent to Spiral). - Surge domains — the same names label the per-type surge
domainblocks and the surgeZonedomainfield.
{"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.
addr — the location record’s doors
Section titled “addr — the location record’s doors”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:
-
namestring - Expression for the SHORT NAME. Default helixAddrName() — the addr's `name` field.
-
addrstring - Expression for the FULL ADDRESS. Default helixAddrText() — the non-empty of name, line1, line2, area, postcode, joined in that order.
-
atstring - Expression for the COORDINATE. Default helixAddrAt() — the addr's `at` field ([lat,lng]).
-
telstring - Expression for the TELEPHONE. Default helixAddrTel() — the addr's `tel` field.
-
areastring - 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.
helix.contactCentre
Section titled “helix.contactCentre”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.
-
searchPathsstring - 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.
Related
Section titled “Related”- contractParamsObject
- projectObject — the
contractReflink from a project to a contract record - Helix technical overview
helix.hdr
Section titled “helix.hdr”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
Section titled “helix.archive”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 } }}-
enabledboolean - Pause switch for a present block (default true). The usual off switch is to omit the block or set helix.archive: null.
-
cadenceduration - How often a sweep runs (seconds, or "D#HH:MM:SS"). Default 60s.
-
batchnumber - Max completed entities archived per sweep (default 25).
-
auditRowsnumber - Max audit rows moved per sweep across all drains (default 2000).
-
dbNamestring - Override the built-in sink database name (default HELIX_ARCHIVE_DB / "helixArchive").
-
completedDelayduration - 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
Section titled “helix.field”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? } - 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
Section titled “helix.fed”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"] }-
blockstring - 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.