Skip to content

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

local.<moduleId>.event2.<eventName>.<entityId>

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:

{
  "time": "2026-08-27T02:15:04.123Z",
  "data": { "...payload fields..." }
}
  • time is when the transition happened (RFC3339 UTC), not when the message was published.
  • data is 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.

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.