Summary
Triggers are event-based notifications that fire when a condition is met on an observed resource. Unlike cyclic subscriptions (which push data at fixed intervals regardless of changes), triggers only fire when something specific happens - a value changes, reaches a target, enters a range, or leaves a range.
Triggers deliver events via SSE, like cyclic subscriptions.
Proposed solution
1. POST /api/v1/{entity-path}/triggers
Create a new trigger on an observed resource.
Applies to entity types: Components, Apps
Path parameters:
| Parameter |
Type |
Required |
Description |
{entity-path} |
URL segment |
Yes |
e.g., apps/temp_sensor |
Request body:
{
"resource": "/api/v1/apps/temp_sensor/data/temperature",
"trigger_condition": {
"condition_type": "LeaveRange",
"lower_bound": 20.0,
"upper_bound": 30.0
},
"path": "/data",
"protocol": "sse",
"multishot": true,
"lifetime": 3600
}
| Field |
Type |
Required |
Description |
resource |
string (URI) |
Yes |
Full URI of the resource to observe |
trigger_condition |
TriggerCondition |
Yes |
Condition that fires the trigger (see below) |
path |
string (JSON Pointer) |
No |
Sub-element within the resource to observe (e.g., /data to watch the value field). If omitted, the entire resource is observed. |
protocol |
string |
No |
Transport protocol. Only "sse" is supported. Default: "sse". |
multishot |
boolean |
No |
If true, trigger fires on every condition match. If false (default), trigger fires once then auto-terminates. |
persistent |
boolean |
No |
If true, trigger survives server restart (stored persistently). Default: false. |
lifetime |
integer |
No |
Seconds until auto-termination. If omitted, trigger lives until manually deleted or server restarts (for non-persistent). |
log_settings |
object |
No |
Auto-logging configuration when trigger fires (severity, marker text). |
Trigger Conditions
The trigger_condition object has a condition_type discriminator:
OnChange - fires on any value change
{
"condition_type": "OnChange"
}
No additional fields. Fires whenever the observed value differs from the previous reading.
OnChangeTo - fires when value changes to a specific target
{
"condition_type": "OnChangeTo",
"target_value": 100.0
}
| Field |
Type |
Description |
target_value |
any |
The target value to match |
EnterRange - fires when value enters a range
{
"condition_type": "EnterRange",
"lower_bound": 20.0,
"upper_bound": 30.0
}
| Field |
Type |
Description |
lower_bound |
number |
Lower boundary (inclusive) |
upper_bound |
number |
Upper boundary (inclusive) |
Fires when the value transitions from outside [lower_bound, upper_bound] to inside.
LeaveRange - fires when value leaves a range
{
"condition_type": "LeaveRange",
"lower_bound": 20.0,
"upper_bound": 30.0
}
Fires when the value transitions from inside [lower_bound, upper_bound] to outside.
Response 201 Created:
{
"id": "trig_001",
"status": "active",
"observed_resource": "/api/v1/apps/temp_sensor/data/temperature",
"event_source": "/api/v1/apps/temp_sensor/triggers/trig_001/events",
"trigger_condition": {
"condition_type": "LeaveRange",
"lower_bound": 20.0,
"upper_bound": 30.0
}
}
| Field |
Type |
Description |
id |
string |
Server-generated trigger identifier |
status |
string |
active or terminated |
observed_resource |
string (URI) |
The resource being observed |
event_source |
string (URI) |
URI to connect to for receiving trigger events |
trigger_condition |
TriggerCondition |
The condition being evaluated |
Error responses:
| Status |
Error Code |
When |
400 |
invalid-parameter |
Invalid resource URI, unknown condition type, missing required fields, lower_bound > upper_bound |
404 |
entity-not-found |
Entity doesn't exist |
501 |
not-implemented |
Feature not implemented |
503 |
service-unavailable |
Server is at capacity |
2. GET /api/v1/{entity-path}/triggers
List all triggers for an entity.
Response 200 OK:
{
"items": [
{
"id": "trig_001",
"status": "active",
"observed_resource": "/api/v1/apps/temp_sensor/data/temperature",
"event_source": "/api/v1/apps/temp_sensor/triggers/trig_001/events",
"trigger_condition": { "condition_type": "OnChange" }
}
]
}
3. GET /api/v1/{entity-path}/triggers/{id}
Read a single trigger's details.
Response 200 OK: Returns a single Trigger object.
Error: 404 if trigger doesn't exist.
4. PUT /api/v1/{entity-path}/triggers/{id}
Update the lifetime of an existing trigger.
Request body:
Response 200 OK: Returns the updated Trigger.
Error: 404 if trigger doesn't exist, 400 if invalid lifetime.
5. DELETE /api/v1/{entity-path}/triggers/{id}
Remove a trigger. Closes the SSE event stream if connected.
Response 204 No Content
Error: 404 if trigger doesn't exist.
6. GET /api/v1/{entity-path}/triggers/{id}/events
SSE event stream that delivers trigger events.
Event format (EventEnvelope):
data: {"timestamp":"2026-02-14T10:30:00Z","payload":{"id":"temperature","data":{"data":35.2}}}
For single-shot triggers (multishot: false): one event is sent, then the stream closes and the trigger status transitions to terminated.
For multi-shot triggers: events continue until the trigger is deleted, its lifetime expires, or the server restarts.
Observable Resources
Triggers can observe these resource types:
data - topic values (most common)
faults - fault status changes
operation executions - execution status changes
script executions - script execution status changes
updates - software update status changes
locks - lock state changes
Business Rules
- Persistent triggers cannot be created on resources that require a lock - return
400 if attempted
- Trigger status transitions:
active → terminated (single-shot after firing, lifetime expiry, or manual delete)
log_settings is optional - if provided, the server automatically creates a log entry when the trigger fires
Additional context
Architecture
- Create a
TriggerManager class that stores trigger definitions and evaluates conditions
- Subscribe to the observed ROS 2 topic on trigger creation
- On each message: extract the value at
path (JSON pointer), compare with previous value, evaluate condition
- If condition fires: push EventEnvelope via SSE, handle single-shot termination
Reuse existing SSE infrastructure
Same SSE pattern as SSEFaultHandler and cyclic subscriptions. Extract common utilities.
Route registration
srv->Post((api_path("/apps") + R"(/([^/]+)/triggers$)"), handler);
srv->Get((api_path("/apps") + R"(/([^/]+)/triggers$)"), handler);
srv->Get((api_path("/apps") + R"(/([^/]+)/triggers/([^/]+)$)"), handler);
srv->Put((api_path("/apps") + R"(/([^/]+)/triggers/([^/]+)$)"), handler);
srv->Delete((api_path("/apps") + R"(/([^/]+)/triggers/([^/]+)$)"), handler);
srv->Get((api_path("/apps") + R"(/([^/]+)/triggers/([^/]+)/events$)"), handler);
// Same for /components/
Tests
- Unit test: create OnChange trigger → 201
- Unit test: create EnterRange trigger with bounds → 201
- Unit test: invalid condition type → 400
- Unit test: lower_bound > upper_bound → 400
- Unit test: single-shot trigger fires once then terminates
- Unit test: multi-shot trigger fires repeatedly
- Unit test: lifetime expiry auto-terminates
- Integration test: create trigger on live topic, verify events fire on value change
Summary
Triggers are event-based notifications that fire when a condition is met on an observed resource. Unlike cyclic subscriptions (which push data at fixed intervals regardless of changes), triggers only fire when something specific happens - a value changes, reaches a target, enters a range, or leaves a range.
Triggers deliver events via SSE, like cyclic subscriptions.
Proposed solution
1.
POST /api/v1/{entity-path}/triggersCreate a new trigger on an observed resource.
Applies to entity types: Components, Apps
Path parameters:
{entity-path}apps/temp_sensorRequest body:
{ "resource": "/api/v1/apps/temp_sensor/data/temperature", "trigger_condition": { "condition_type": "LeaveRange", "lower_bound": 20.0, "upper_bound": 30.0 }, "path": "/data", "protocol": "sse", "multishot": true, "lifetime": 3600 }resourcestring(URI)trigger_conditionTriggerConditionpathstring(JSON Pointer)/datato watch the value field). If omitted, the entire resource is observed.protocolstring"sse"is supported. Default:"sse".multishotbooleantrue, trigger fires on every condition match. Iffalse(default), trigger fires once then auto-terminates.persistentbooleantrue, trigger survives server restart (stored persistently). Default:false.lifetimeintegerlog_settingsobjectTrigger Conditions
The
trigger_conditionobject has acondition_typediscriminator:OnChange- fires on any value change{ "condition_type": "OnChange" }No additional fields. Fires whenever the observed value differs from the previous reading.
OnChangeTo- fires when value changes to a specific target{ "condition_type": "OnChangeTo", "target_value": 100.0 }target_valueEnterRange- fires when value enters a range{ "condition_type": "EnterRange", "lower_bound": 20.0, "upper_bound": 30.0 }lower_boundupper_boundFires when the value transitions from outside
[lower_bound, upper_bound]to inside.LeaveRange- fires when value leaves a range{ "condition_type": "LeaveRange", "lower_bound": 20.0, "upper_bound": 30.0 }Fires when the value transitions from inside
[lower_bound, upper_bound]to outside.Response
201 Created:{ "id": "trig_001", "status": "active", "observed_resource": "/api/v1/apps/temp_sensor/data/temperature", "event_source": "/api/v1/apps/temp_sensor/triggers/trig_001/events", "trigger_condition": { "condition_type": "LeaveRange", "lower_bound": 20.0, "upper_bound": 30.0 } }idstringstatusstringactiveorterminatedobserved_resourcestring(URI)event_sourcestring(URI)trigger_conditionTriggerConditionError responses:
400invalid-parameterlower_bound>upper_bound404entity-not-found501not-implemented503service-unavailable2.
GET /api/v1/{entity-path}/triggersList all triggers for an entity.
Response
200 OK:{ "items": [ { "id": "trig_001", "status": "active", "observed_resource": "/api/v1/apps/temp_sensor/data/temperature", "event_source": "/api/v1/apps/temp_sensor/triggers/trig_001/events", "trigger_condition": { "condition_type": "OnChange" } } ] }3.
GET /api/v1/{entity-path}/triggers/{id}Read a single trigger's details.
Response
200 OK: Returns a singleTriggerobject.Error:
404if trigger doesn't exist.4.
PUT /api/v1/{entity-path}/triggers/{id}Update the
lifetimeof an existing trigger.Request body:
{ "lifetime": 7200 }Response
200 OK: Returns the updatedTrigger.Error:
404if trigger doesn't exist,400if invalid lifetime.5.
DELETE /api/v1/{entity-path}/triggers/{id}Remove a trigger. Closes the SSE event stream if connected.
Response
204 No ContentError:
404if trigger doesn't exist.6.
GET /api/v1/{entity-path}/triggers/{id}/eventsSSE event stream that delivers trigger events.
Event format (EventEnvelope):
For single-shot triggers (
multishot: false): one event is sent, then the stream closes and the trigger status transitions toterminated.For multi-shot triggers: events continue until the trigger is deleted, its lifetime expires, or the server restarts.
Observable Resources
Triggers can observe these resource types:
data- topic values (most common)faults- fault status changesoperation executions- execution status changesscript executions- script execution status changesupdates- software update status changeslocks- lock state changesBusiness Rules
400if attemptedactive→terminated(single-shot after firing, lifetime expiry, or manual delete)log_settingsis optional - if provided, the server automatically creates a log entry when the trigger firesAdditional context
Architecture
TriggerManagerclass that stores trigger definitions and evaluates conditionspath(JSON pointer), compare with previous value, evaluate conditionReuse existing SSE infrastructure
Same SSE pattern as
SSEFaultHandlerand cyclic subscriptions. Extract common utilities.Route registration
Tests