Messaging¶
All inter-process communication goes through NATS. This page is the operator and integrator map: subject conventions, the messaging contexts, and every request-reply endpoint grouped by module.
Default module ids are shown. A deployment may override --id (for example recorder.prod); subjects then use that id in the first slot after local.
Subject conventions¶
Publish / subscribe¶
| Context | Kind | Typical producers | Typical consumers |
|---|---|---|---|
data |
pub/sub | data sources | recorder, aggregator, CLI subscribe |
data_status |
pub/sub | aggregator (per-batch stats on the source channel's ref) | trigger, gate |
status |
pub/sub | every module | trigger, gate, CLI |
event2 |
pub/sub (core NATS) | typed event producers | trigger, observers |
measurement |
pub/sub | flower | recorder |
config |
pub/sub | configurer | every module's config handler |
config_status |
pub/sub | configurer and target modules | CLI, API |
cmd |
request-reply | CLI, trigger, other modules | the owning module |
Events are fire-and-forget on core NATS: a consumer that is down misses them. Current state always lives on config status or periodic telemetry - see Events.
Commands¶
Six dot-separated segments after local. Wildcards appear in some registrations (*) but each live call resolves to a concrete subject.
Handlers use NATS micro services. Treat these response headers as failures:
Nats-Micro-Error- descriptionNats-Micro-Error-Code- HTTP-like code (400,404,409,429,500, ...)
Successful replies are ordinary NATS responses (JSON, or raw bytes where noted).
What a declared command takes and answers is in the generated commands reference
(RFC-040), rendered from the committed schema/command/ tree; a command not listed there is
not declared in this build.
Readiness probe¶
Before other modules proceed they poll:
| Subject | Request | Response |
|---|---|---|
local.nats.cmd.app.main.status |
empty | JSON status string: "OK" when the broker is ready |
CLI to NATS¶
| CLI command | Subject |
|---|---|
algol config ls / info |
local.cfg.cmd.store.main.list_config_status |
algol config update |
local.cfg.cmd.store.main.update |
algol config delete |
local.cfg.cmd.store.main.update (delete operation) |
algol config patch |
local.cfg.cmd.store.main.patch |
algol config commit |
local.cfg.cmd.store.main.commit |
algol config delete --bundle |
local.cfg.cmd.store.main.delete_bundle |
algol bundle update |
local.cfg.cmd.bundle.main.update |
algol bundle activate |
local.cfg.cmd.bundle.main.activate |
algol bundle delete |
local.cfg.cmd.bundle.main.delete |
algol manifest import |
local.cfg.cmd.manifest.main.import |
algol manifest export |
local.cfg.cmd.manifest.main.export |
algol recording ls |
local.recorder.cmd.recstorage.main.recording_list |
algol recording info |
local.recorder.cmd.recstorage.main.recording_get |
algol trigger publish |
local.flower.cmd.flow.main.measurement_event_publish |
algol nats ls |
local.nats.cmd.auth.main.ls |
algol nats enrol |
local.nats.cmd.auth.main.enrol |
algol nats revoke |
local.nats.cmd.auth.main.revoke |
algol secret set |
local.cfg.cmd.store.main.update (the declaration, when there is none yet), then local.cfg.cmd.secret.<id>.set |
algol secret get |
local.cfg.cmd.secret.<id>.get |
algol command ls / info |
none: the registry compiled into the CLI (commands reference) |
algol secret ls / rm |
config ls cfg.secret.* / config delete cfg.secret.<id> |
algol command <ref> |
local.<moduleId>.cmd.<componentName>.<componentId>.<command> - any endpoint, by attribute ref |
Sys, board-controller, audio, retx, and export start/stop have no verb of their own. Reach them with algol command (see CLI) or from a trigger command / jsonCommand action.
Configurer (cfg)¶
Health component: cfg.app.main. Bundle and manifest subjects use reserved pseudo-components cfg.bundle.main and cfg.manifest.main (not config types).
status¶
| Subject | local.cfg.cmd.app.main.status |
| Request | empty |
| Response | JSON component-status string ("OK", "WAIT", "ERROR", ...) |
| CLI | none |
list_config_status¶
| Subject | local.cfg.cmd.store.main.list_config_status |
| Request |
{
"filter": "sys.file.*",
"include": {
"startupConfig": false,
"intentPublishedConfig": false,
"intentConfigStatus": false,
"effectivePublishedConfig": false,
"effectiveConfigStatus": false
}
}
| Response | {"result": [ ...records... ]} |
| CLI | algol config ls, algol config info |
Each record carries ref, managedBy, bundle, and the included stage bodies. See Configuration for the five stages.
The summary include is a sixth view of those same five stages rather than a sixth stage: each one as a content digest plus what a row is rendered from - the enabled state, the request id, the status code and message, whether a status echoed the body it was sent - together with whether the intent is uncommitted and whether the config is read-only. It is what a listing asks for, and it asks for nothing else: the reply is a single message holding every config the filter matched, and a file config carrying inline content makes a body two orders of magnitude larger than a config body otherwise is. Bodies are read one ref at a time, which is what algol config info does.
update¶
| Subject | local.cfg.cmd.store.main.update |
| Request |
{
"ref": "<target component>",
"op": 0,
"version": 0,
"requestId": "cli-...",
"bundle": "",
"config": { "type": "<configType>" },
"force": false
}
op 0 is update. Delete uses the same subject with the delete operation and no config body.
| Response | ref, version, status, requestId, changedAt, config |
| CLI | algol config update, algol config delete |
patch¶
| Subject | local.cfg.cmd.store.main.patch |
| Request | ref, optional enabled, properties, version, requestId, force |
| Response | same shape as update |
| CLI | algol config patch |
At least one of enabled, properties, or force is required. The CLI sets properties with -p/--property name=value (type taken from the existing bag) and toggles enabled with --enable/--disable.
commit¶
| Subject | local.cfg.cmd.store.main.commit |
| Request | {"filter": "<wildcard ref>", "bundle": "", "force": false} |
| Response | empty on success |
| CLI | algol config commit |
delete_bundle¶
| Subject | local.cfg.cmd.store.main.delete_bundle |
| Request | {"bundle": "<bundleName>"} |
| Response | {"result": [ { "ref": "...", "status": ..., "error": "..." } ]} |
| CLI | algol config delete --bundle <b> |
Bundle: update / activate / delete¶
| Subject | Request | Response | CLI |
|---|---|---|---|
local.cfg.cmd.bundle.main.update |
id, optional add[], remove[], bundle, description |
id, bundle, description, members[], createdAt, digest, installedAt |
algol bundle update |
local.cfg.cmd.bundle.main.activate |
{"id": "<bundleId>", "adopt": false} |
id, bundle, members[] with per-member status / error / adopted |
algol bundle activate |
local.cfg.cmd.bundle.main.delete |
{"id": "<bundleId>", "force": false} |
id, bundle, stillRunning[], forced |
algol bundle delete |
algol bundle ls / info use list_config_status over *.bundleinfo.*. There is no dedicated bundle-read endpoint.
Manifest: import / check / export¶
| Import subject | local.cfg.cmd.manifest.main.import |
| Request | Raw manifest JSON or gzip-compressed bytes (not wrapped) |
| Headers | X-Algol-Manifest-Adopt: true, X-Algol-Manifest-Replace: true (optional) |
| Response | bundles[], activations[], configs[], removedConfigs[], removedBundles[] |
| CLI | algol manifest import |
| Check subject | local.cfg.cmd.manifest.main.check |
| Request | The import's request, unchanged: same payload, same two headers |
| Response | bundles[], activate[], configs[], removedConfigs[], removedBundles[], replace |
| CLI | - |
The dry run of that same import: it runs the import's own validation, writes nothing, and
reports what an apply would do. It is what lets a caller name the configs a --replace restore
would withdraw before an operator confirms it - a set that depends on the device, not on the
manifest, so nothing but the store can derive it.
| Export subject | local.cfg.cmd.manifest.main.export |
| Request | optional bundles[], noActivate (empty body = full backup) |
| Response | Gzip-compressed manifest (Content-Type: application/gzip) |
| CLI | algol manifest export |
Secret: set / get¶
The value of a secret travels these two subjects only (RFC-001 D2), core NATS request-reply, answered into the requester's inbox. The requester is the reply subject's inbox prefix (_INBOX.<moduleId>, _INBOX.userkey.<key>, RFC-034 D8).
| Set subject | local.cfg.cmd.secret.<id>.set |
| Request | {"value": "<base64>"} |
| Response | {"changed": "<RFC3339>", "size": 28} |
| Errors | 404 not declared, 400 the value does not fit the declared kind, 500 the store failed |
| CLI | algol secret set <id> (--value V \| --from-file F \| -), which declares first through update when the id has none |
| Get subject | local.cfg.cmd.secret.<id>.get |
| Request | {} |
| Response | {"kind": "string", "changed": "<RFC3339>", "value": "<base64>"} |
| Errors | 404 not declared, 403 the requester holds no dispatched config referencing the secret from a type the declaration admits (a person is never refused), 409 no value set, 500 the store failed |
| CLI | algol secret get <id> (prints the value, raw) |
update and patch on a cfg.secret.* ref are answered with 403 unless the requester is a person: declaring and scoping a secret is a person's privilege (RFC-001 D10).
Sys (sys)¶
restart¶
| Subject | local.sys.cmd.systemd.ctl.restart |
| Request | Raw UTF-8 systemd unit name (e.g. systemd-resolved.service) |
| Response | empty |
| CLI | none; trigger command |
Restarts any systemd unit on the host by name - a keymaster capability in its own right, deliberately kept next to the per-component restarts below. Because the target is named in the body, this command is never grantable through a configured role's write set (RFC-035 D8); the per-component restarts are the grantable form.
systemdscript start¶
| Subject | local.sys.cmd.systemdscript.<id>.start |
| Request | ignored |
| Response | "ok" |
| Purpose | Run a listen-mode script on demand |
systemdapp start¶
| Subject | local.sys.cmd.systemdapp.<id>.start |
| Request | ignored |
| Response | "ok" |
| Purpose | Mount the rootfs and run a listen-mode app workload |
systemdscript / systemdapp restart¶
| Subject | local.sys.cmd.systemdscript.<id>.restart, local.sys.cmd.systemdapp.<id>.restart |
| Request | ignored |
| Response | "ok" |
| Errors | 404 unknown component, 500 disabled or the unit operation failed |
| Purpose | Restart the component's unit: a running unit is bounced, a loaded exited/failed one re-activated, a never-started one started. Not limited to listen mode. |
The grantable form of restart (RFC-035 D8): a
command names its component in the subject or is not grantable through a role's write set.
The module-level restart by unit name stays alongside, for keymasters.
systemdmount release¶
| Subject | local.sys.cmd.systemdmount.<slot>.release |
| Request | ignored |
| Response | "ok" (acceptance only; unmount is asynchronous) |
| Purpose | Unmount the slot without disabling the config, so an operator can swap media |
Watch config status for WAIT released... before pulling the disk. See systemdmount.
GSM send_sms¶
| Subject | local.sys.cmd.gsm.<connId>.send_sms |
| Request | {"number": "+...", "text": "..."} |
| Response | "ok" |
| Errors | 429 if the SMS queue is full |
GSM reset¶
| Subject | local.sys.cmd.gsm.<connId>.reset |
| Request | ignored |
| Response | "ok" |
| Errors | 404 unknown gsm component, 500 no modem or the reset failed |
| Purpose | Hardware-reset the connection's modem |
The per-component reshape of the retired reset-all-modems command
(RFC-035 D8). The connection does not have
to be active: a modem that has lost registration cannot bring one up, and that is the
state a reset is for. Reset attempts and errors are counted per modem on the owning
connection's status (modemResetCounter / modemResetErrCounter), against the modem's
physical device rather than its object path, so a reset does not zero the count of itself.
LED set¶
| Subject | local.sys.cmd.led.<indicatorId>.set |
| Request | JSON map of element name to pattern: { "green": "on", "red": "blink" } |
| Response | "ok" |
Omitted elements turn off. {} clears to dark where allowed. See led.
Recorder (recorder)¶
Catalog subjects are served on the recstorage component, which owns the medium and the catalog; the CLI addresses it with --recorder (default recorder.recstorage.main). The same component is the recording's owner: its ref is stamped into every Parquet file. It was named recorder.default.main before RFC-035. The rename re-keys nothing already written - archive and Recording refs derive from the recorder's module id, not its full ref - and recordings stamped with the old name stay managed (listed, counted, exported and cleaned up), so a medium upgraded in place holds both spellings and loses nothing.
recording_list¶
| Subject | local.recorder.cmd.recstorage.main.recording_list |
| Request |
{
"filter": { "measurement": "flower.measurement.*", "channel": "*.*.*" },
"page": { "offset": 0, "limit": 100 },
"order": "asc"
}
limit defaults to 100, max 500.
| Response | recordings[], offset, total |
| CLI | algol recording ls |
Each recording includes recRef, ref, measurementRef, archiveRef, channelRef, channelType, freq, url, start, last, deleted, archiveIdx, archiveLen, size, digest, and context (deviceId, serialNr, measurementSequenceNr).
recording_get¶
| Subject | local.recorder.cmd.recstorage.main.recording_get |
| Request | {"ref": "<recording component ref>"} |
| Response | {"recording": { ... }} or 404 |
| CLI | algol recording info <recording-ref> |
Export start / stop¶
One pair per export profile:
| Profile | Subjects | Request | Response |
|---|---|---|---|
s3upload |
local.recorder.cmd.s3upload.<id>.start / .stop |
ignored | "ok" |
usbexport |
local.recorder.cmd.usbexport.<id>.start / .stop |
ignored | "ok" |
Idempotent. The reply means the command was accepted, not that the export finished - watch profile telemetry / config status. Errors: 404 unknown profile, 409 stop when not runnable.
Flower (flower)¶
measurement_event_publish¶
| Subject | local.flower.cmd.flow.main.measurement_event_publish |
| Request |
{
"event": {
"ref": "<trigger component>",
"measurementRef": null,
"timestamp": "...",
"start": "...",
"deadline": "...",
"triggerType": "start|stop|startExtend",
"reason": "...",
"dataTimestamp": "..."
}
}
| Response | |
{
"result": {
"result": "event_ignored|new_measurement_triggered|deadline_updated",
"measurementRef": "<component or null>"
}
}
| CLI | algol trigger publish |
| Also called by | the trigger module |
Board controller (bc)¶
Register facade¶
| Subject | local.bc.cmd.<connector>.<id>.<attr> matching configured register refs |
| Valid targets | component id reg (legacy main is treated as reg) |
| Read | empty body -> {"status":"ok","value": <uint16>} |
| Write | ASCII decimal uint16 -> {"status":"ok"} |
Register ref form: bc.<connectorName>.reg.<registerName>.
execute_cmd / execute_cmds¶
| Single | local.bc.cmd.main.execute.cmd |
| Batch | local.bc.cmd.main.execute.cmds |
A single command is a JSON object discriminated by "type": writeValue, readValue, readValues, directReadValue, directReadValues, directWriteValue, directWriteValues, directWriteBytes, with fields such as registerRef, unitId, registerId, value, options.timeout.
Batch request: {"timeout": 5.0, "continueOnError": false, "commands": [ ... ]}. Response: {"status":"ok","value":{"result":[[...]], "errors":[]}}.
Direct register¶
| Subject | local.bc.cmd.<connectorName>.direct.reg_<registerId> |
| Read / write | empty body, or ASCII decimal uint16 |
| Purpose | Bypass register-ref indirection; the attribute must start with reg_ |
Audio (audio)¶
| Subject | local.audio.cmd.player.main.play |
| Request | WAV filename only (basename, .wav, no directories), e.g. alarm.wav |
| Response | "ok" after playback completes (blocking; default timeout 5 min) |
| Errors | 404 missing file, 400 invalid name |
Files come from the module's configured audio directory.
RETX (retx)¶
| Subject | local.retx.cmd.app.main.dump |
| Request | ASCII integer: number of raw bytes to capture from the live stream |
| Response | {"status":"ok\|ignored\|error","message":"..."} |
"ok" returns immediately with an output file path; capture runs asynchronously. Ignored when dump is disabled, the body is invalid, the size exceeds the module cap, or a dump is already in progress.
NATS broker (nats)¶
| Subject | Request | Response |
|---|---|---|
local.nats.cmd.app.main.status |
empty | JSON "OK" / "WAIT" / ... |
This is a request-reply health probe, not a telemetry message. See Telemetry.
Auth verbs: ls / enrol / revoke¶
Served under --auth only (RFC-034 D6): the identity registry's read and write surface. Publishable by role keymaster (the boot's root key, or an enrolled keymaster); every request is signed by the caller's key, and the service acts on the key that signed, not on the connection that carried it.
| Subject | local.nats.cmd.auth.main.ls / .enrol / .revoke |
| Request |
{
"key": "<caller's public NKey, U...>",
"body": {
"verb": "enrol",
"issued": "2026-08-30T18:00:00Z",
"userKey": {"key": "UQRS...", "label": "lukas-laptop", "role": "superuser"}
},
"sig": "<base64url ed25519 signature over the body bytes>"
}
| Response | ls: {"modules": [{"name", "expected", "key", "bound", "boundBy", "revoked"}], "userKeys": [{"key", "label", "role", "enrolledBy", "enrolled"}]}; enrol / revoke: {"disconnected": n} |
| CLI | algol nats ls / enrol / revoke (--nats-id for a nats app under another --id) |
body is verified as the bytes sent, so a client marshals it once and signs exactly that. enrol and revoke name either userKey (key; label and role on enrol) or module (name; key on enrol); ls takes no arguments. issued must lie within five minutes of the broker's clock and a signature is answered once. Errors: 400 malformed body or an argument the registry rejects, 401 unknown key / bad signature / replay, 403 role may not administer or grant, 404 no such entry, 500 registry write failed. Wire types: lib/flow/messaging/service/auth.
Modules with no request-reply surface¶
These publish and subscribe only (config, status, data, events). They call endpoints above but do not expose their own.
| Module id | Role |
|---|---|
trigger |
Client: fires command / jsonCommand / patchConfig actions at any cmd subject; publishes measurement events to flower |
gate |
Cloud connectivity |
aggregator |
Per-batch data_status stats |
algold |
Process supervisor |
cli |
Client wrapping configurer / recorder / flower endpoints |
secomea |
VPN; config / status |
gps |
GNSS source; config / status / data |
chrp |
LoRa (ChirpStack); config / status / events |
chrony |
NTP; config / status |
Trigger as a command client¶
A trigger command or jsonCommand action sends request-reply to any configured cmdRef (full attribute ref including the verb), for example sys.systemdmount.disk01.release or recorder.usbexport.stick01.start. Default timeout is 5 seconds. patchConfig goes to the configurer patch endpoint.