Skip to content

Implement Triggers - event-based conditional data delivery #203

@bburda

Description

@bburda

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:

{
  "lifetime": 7200
}

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: activeterminated (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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions