Events¶
Events are the third message class on the platform, next to data (batched sensor samples) and status (periodic telemetry). An event announces that something just happened. It is not a snapshot of current state.
This page is the operator and integrator catalog. The design decisions live in RFC-025.
How events work¶
Subject¶
The component ref is <moduleId>.<eventName>.<entityId>:
- event name - the kind of transition (
disk_mounted,sms_received,custom_door_opened). This is also the downstream table name. - entity id - the instance the event is about (a mount slot, a GSM connection, a trigger id).
Filter and route by the ref. Read details from the payload.
Envelope¶
Every registered event travels as:
timeis when the transition happened (RFC3339 UTC), not when the message was published.datais the typed payload only. Addressing lives on the subject, not in the body.
Custom events (see below) may add an optional types map so a number is stored as an integer rather than a float.
The on-device context token is event2. Gate forwards those messages onto the cloud event subject, stamping Content-Type: application/vnd.algol.event2+json. The legacy on-device event context is retired.
Rules that matter in operation¶
| Rule | Meaning |
|---|---|
| One name per transition | disk_mounted and disk_unmounted are two events, not one event with a state field. |
| Global names | The same event name has the same schema everywhere. Do not reuse a name with a different field set. |
| An event is an edge, not a state | Current state lives on config status or periodic health telemetry. Do not reconstruct "is the disk mounted?" from events. |
| Events are not durable | A consumer that is down misses them. That is safe because no event is the only record of an occurrence. |
| Registered vs custom | A name is either a registered type in the table below, or custom_<label>. Anything else is refused at config time. |
Trigger expressions subscribe with the event_* functions (event_val, event_raw, ...). Those functions read event2. See Trigger.
Registered events¶
Subject pattern: local.<moduleId>.event2.<eventName>.<entityId>
| Event | Producer | Entity id | About |
|---|---|---|---|
disk_mounted |
sys |
mount slot (disk01, ...) |
systemdmount |
disk_unmounted |
sys |
mount slot | systemdmount |
led_state_changed |
sys |
LED id | led |
script_started |
sys |
script id | systemdscript |
script_exited |
sys |
script id | systemdscript |
sms_sent |
sys |
GSM connection id | gsm |
sms_received |
sys |
GSM connection id | gsm |
lora_uplink_received |
chrp |
D<devEUI> (lowercase) |
LoRa uplink |
http_request_received |
any module hosting endpoints (webui) |
endpoint id | httpendpoint |
recording_completed |
recorder |
main |
a finished Recording part |
config_changed |
cfg |
Base32 of the config ref (the ref is in the payload) | any config, of any type |
teapot_prepared |
examples / tests | teapot id | probe type, not a production producer |
Retired, frozen schemas with no producer:
| Event | Replaced by |
|---|---|
led_changed (color, pattern) |
led_state_changed |
Older names that no longer appear on the wire: led (pre-led_changed), loraup (now lora_uplink_received), RecordingComplete on the legacy event context (now recording_completed on event2).
disk_mounted¶
Published when a systemdmount slot becomes mounted.
Subject: local.sys.event2.disk_mounted.<slot>
| Field | Type | Required | Description |
|---|---|---|---|
diskSelector |
string | yes | DiskSelector is the /dev/disk/by-* selector the slot mounts, in systemdmount's own spelling and never an absolute path: "by-label/ALGOL-REC", not "/dev/disk/by-label/ALGOL-REC". Canonicalized, so a config written in the tolerated absolute form still reports the short one. |
fsType |
string | yes | FsType is the filesystem type the kernel found, read from host mountinfo - which is not necessarily the one the config asked for. |
targetDir |
string | yes | TargetDir is the pool slot mount target. |
readOnly |
bool | yes | ReadOnly reports that the mount is read-only, whatever the config asked for. |
The slot's current mounted/unmounted/released state is on config status, not in this event.
disk_unmounted¶
Published when a mount slot is unmounted.
Subject: local.sys.event2.disk_unmounted.<slot>
| Field | Type | Required | Description |
|---|---|---|---|
reason |
string | yes | Reason says why the filesystem went away. One of released, deviceRemoved, configDisabled, configDeleted, configChanged, failed. |
targetDir |
string | yes | TargetDir is the pool slot mount target the filesystem was at. |
released is the operator path (the release command). deviceRemoved is a yanked disk. Do not treat this event as "safe to pull" - wait for config status WAIT released... after a release.
led_state_changed¶
Published when an LED's accepted state actually changes. Setting the same state again is silent.
Subject: local.sys.event2.led_state_changed.<ledId>
| Field | Type | Required | Description |
|---|---|---|---|
green |
string | no | Green is the pattern the green element plays; omitted if there is no green element. One of off, on, blink. |
red |
string | no | Red is the pattern the red element plays; omitted if there is no red element. One of off, on, blink. |
yellow |
string | no | Yellow is the pattern the yellow element plays; omitted if there is no yellow element. A distinct yellow element, not green and red lit together - that combination is reported as the two elements it is. One of off, on, blink. |
The live pattern of each element is also on the LED's status telemetry. See led.
script_started¶
Published when a systemdscript process starts.
Subject: local.sys.event2.script_started.<scriptId>
| Field | Type | Required | Description |
|---|---|---|---|
cmd |
string | yes | Cmd is the executable the wrapper ran. |
args |
string array | yes | Args are the arguments it was started with. |
script_exited¶
Published when a script process exits.
Subject: local.sys.event2.script_exited.<scriptId>
| Field | Type | Required | Description |
|---|---|---|---|
cmd |
string | yes | Cmd is the executable the wrapper ran. |
args |
string array | yes | Args are the arguments it was started with. |
exit_code |
int | yes | ExitCode follows the POSIX convention: 128 + signal number when the process was killed by a signal. |
signal |
int | yes | Signal is the number of the signal that killed the process, 0 when it exited on its own. |
sms_sent¶
Published after an outgoing SMS attempt on a GSM connection (including the send_sms command).
Subject: local.sys.event2.sms_sent.<connId>
| Field | Type | Required | Description |
|---|---|---|---|
recipient |
string | yes | Recipient is the number the SMS was sent to. |
text |
string | yes | Text of the SMS. |
retries |
int | yes | Retries is how many send attempts preceded this outcome. |
success |
bool | yes | Success reports whether the send succeeded. |
error |
string | no | Error is the send failure, empty on success. |
sms_received¶
Published when the modem delivers an inbound SMS.
Subject: local.sys.event2.sms_received.<connId>
| Field | Type | Required | Description |
|---|---|---|---|
sender |
string | yes | Sender is the number the SMS came from. |
text |
string | yes | Text of the SMS. |
simSlot |
int | yes | SimSlot is the SIM slot the SMS arrived on. |
iccid |
string | yes | Iccid identifies the SIM the SMS arrived on. |
lora_uplink_received¶
Published by ChirpStack when an end-device uplink is accepted.
Subject: local.chrp.event2.lora_uplink_received.D<deveui>
The DevEUI is lowercased and prefixed with D so it is a valid component id (same convention as LoRa telemetry).
| Field | Type | Required | Description |
|---|---|---|---|
raw |
string | yes | Raw is the frame payload as ChirpStack delivered it, base64. |
payload |
string | yes | Payload is the ChirpStack-decoded object, JSON re-encoded as base64; empty when the device profile decodes no object. |
profile |
string | yes | Profile is the ChirpStack device profile name. |
deviceName |
string | yes | DeviceName is the ChirpStack device name. |
fCnt |
int | yes | FCnt is the LoRaWAN uplink frame counter. |
fPort |
int | yes | FPort is the LoRaWAN port the frame arrived on. |
tags |
object | no | Tags are the device's ChirpStack tags, omitted when the device has none. The keys are operator-defined, so the map is open and the schema states only its value type. Values: string. |
tags is the one nested field here: a time-series ingester keeps only top-level scalars, so it is dropped at storage.
http_request_received¶
Published by a module hosting an httpendpoint when that endpoint accepted a request and answered 202. A refused request (authentication, rate, size, unknown endpoint) publishes nothing. See RFC-042.
Subject: local.<module>.event2.http_request_received.<endpointId>
| Field | Type | Required | Description |
|---|---|---|---|
method |
string | yes | Method is the HTTP request method, for example "POST". |
path |
string | yes | Path is the part of the URL path after the endpoint ref, with a leading slash: for example, a request to /hooks/webui.httpendpoint.doorpanel/door/3 has path "/door/3". Empty when the request addressed the endpoint itself. |
query |
string | yes | Query is the raw query string without the leading "?", as the sender wrote it. Empty when there is none. |
source |
string | yes | Source is the TCP peer address as ip:port. It is the direct peer, not a client named by an X-Forwarded-For header. |
headers |
object | no | Headers are the request headers in canonical form, each with every value it was sent with. Authorization, Proxy-Authorization and Cookie are never present: they are removed before publishing, so a credential does not travel with the event. Omitted when no header is left. Values: string array. |
contentType |
string | yes | ContentType is the Content-Type header as the sender set it, empty when absent. It is the sender's claim; nothing checks the body against it. |
payload |
string | yes | Payload is the request body, standard base64 with padding, whatever the body holds: text, JSON or binary. An empty body is an empty string. |
requestId |
string | yes | RequestId identifies the request; the 202 reply carries the same value, so a sender can match its request to this event. |
payload is the body in base64 whatever it holds. A rule reads a JSON body by decoding it, for example fromJSON(fromBase64(event_val('webui.http_request_received.doorpanel.payload'))).state == "open". headers is nested and dropped at storage, like tags on lora_uplink_received. The event is not durable: a consumer that is down misses it, although the sender already holds its 202.
recording_completed¶
Published when the recorder finishes a Recording part. The lifecycle journal is the durable authority; this event is a notification. A missed event costs a delay, not a Recording.
Subject: local.recorder.event2.recording_completed.main
The id slot is the recorder component id (main), not the Recording ref - the Recording is in the payload.
| Field | Type | Required | Description |
|---|---|---|---|
recordingRef |
string | yes | RecordingRef is the Recording the part belongs to. |
measurementRef |
string | yes | MeasurementRef is the Measurement the Recording records. |
channelRef |
string | yes | ChannelRef is the DataChannel the part holds rows of. |
archiveRef |
string | yes | ArchiveRef is the archive the part is filed under. |
part |
int | yes | Part is the archive index of this part within its channel. |
final |
bool | yes | Final reports that this is the channel's last part. |
relativePath |
string | yes | RelativePath is the part file's path relative to the recording root. |
size |
int | yes | Size is the part file's size in bytes. |
contentDigest |
string | no | ContentDigest is the part file's content digest as algorithm:hex (lib/messaging/digest), empty when no digest was computed. |
Authoritative inventory is algol recording ls / the recording_list endpoint, not this event.
config_changed¶
Published by the configurer when a config's record changed: one of its stages moved (startup file written, intent published, intent status, the module's answer), or the record was removed. It carries the record as config ls shows it after the change - digests and status codes, never a config body.
Subject: local.cfg.event2.config_changed.<Base32(config ref)>
The id slot is the Base32 of the config ref in the component alphabet (a ref has dots, which a subject token cannot carry), so the subject cannot be filtered by config ref - read ref from the payload. cfg.config_changed.* selects every config change.
| Field | Type | Required | Description |
|---|---|---|---|
ref |
string | yes | Ref is the config this event is about: <moduleId>.<configType>.<configId>. |
startupConfigHash |
string | yes | StartupConfigHash is the content digest of the startup file on disk, "algorithm:hex". Empty when the config was never committed. |
intentConfigHash |
string | yes | IntentConfigHash is the content digest of the operator's intent as commit would write it. Empty when nobody authored one: a module-sourced config, or a delete intent. |
intentStatus |
string | yes | IntentStatus is the configurer's answer to the intent (the resolution axis): OK, WAIT, ERROR, DISABLED or DELETED. Empty when there is no intent. |
intentMsg |
string | no | IntentMsg is the message beside IntentStatus, when there is one. |
effectiveConfigHash |
string | yes | EffectiveConfigHash is the content digest of the body the module reports it is running. Empty when the module has not answered, or reported the config deleted. |
effectiveStatus |
string | yes | EffectiveStatus is the module's answer (the apply axis): OK, WAIT, ERROR, DISABLED or DELETED. Empty when the module has not answered, or reported the config deleted. |
effectiveMsg |
string | no | EffectiveMsg is the message beside EffectiveStatus, when there is one. |
The digests are content hashes over the canonical form of each body (the same number bundleinfo reports for a member), so equality between two of them means the same config. Reading a row:
| Question | Answer |
|---|---|
| Committed once? | startupConfigHash is set |
| Current intent committed? | startupConfigHash == intentConfigHash |
Uncommitted (the CLI's *)? |
intentConfigHash is set and differs from startupConfigHash |
| Removed? | every field but ref is empty - no live record looks like that |
| What moved? | compare with the previous row for the same ref |
The configurer holds a changed ref for a short window and publishes the net change, so one operator action (intent published, module answered) is normally one event, and a state that lasted only inside the window is not reported. A configurer restart is silent; a device boot publishes one event per config once each module has answered. The current state stays on the config status axis and in config ls / config info - this event is the edge, not the state.
teapot_prepared¶
Probe / example event used in tests and resolution examples. No production module publishes it.
Subject: local.<module>.event2.teapot_prepared.<teapotId>
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | ID of the teapot. |
label |
string | no | Label of the teapot. |
Custom events¶
An operator-defined event is published by a trigger event action.
Name: must be custom_<label>. custom alone is not a name.
Subject: local.<module>.event2.custom_<label>.<triggerId>
Payload: flat JSON scalars only (string, number, boolean). Nested objects and arrays are refused.
Types: JSON cannot tell 42 from 42.0. A number with no fieldTypes entry is a float. Declare integer fields explicitly:
type = "event"
eventName = "custom_speed"
[fields]
speed = { "@value" = "speed" }
axleCount = 4
[fieldTypes]
axleCount = "int"
Allowed fieldTypes values: string, int, uint, float, bool. Declaring a type a literal cannot have is refused at config time; a captured value that contradicts its declaration is refused when the trigger fires. A registered eventName needs no fieldTypes - and declaring one is refused, because types come from that event's schema.