Skip to content

feat(triggers): implement event-based conditional data delivery (#203)#293

Open
bburda wants to merge 40 commits intomainfrom
feat/triggers-203
Open

feat(triggers): implement event-based conditional data delivery (#203)#293
bburda wants to merge 40 commits intomainfrom
feat/triggers-203

Conversation

@bburda
Copy link
Collaborator

@bburda bburda commented Mar 19, 2026

Pull Request

Summary

Implement SOVD Triggers - event-based notifications that fire when a condition is met on an observed resource. Unlike cyclic subscriptions (fixed-interval push), triggers only fire when something specific happens - a value changes, reaches a target, enters or leaves a range.

New components: ResourceChangeNotifier (async push hub), TriggerManager (lifecycle + condition evaluation), ConditionRegistry (pluggable evaluators), SQLiteTriggerStore (persistence), TriggerHandlers (6 REST endpoints + SSE), TriggerFaultSubscriber, TriggerTopicSubscriber, TriggerTransportProvider (plugin interface).


Issue


Type

  • Bug fix
  • New feature or tests
  • Breaking change
  • Documentation only

Testing

  • 124 new unit tests (ConditionEvaluator, ResourceChangeNotifier, TriggerStore, TriggerManager, TriggerHandlers)
  • 56 new integration tests (data, faults, updates, logs, hierarchy triggers + 3 scenario tests + persistent restart)
  • All 1824 unit tests pass, docs build with -W passes
  • Requirements traceability: REQ_INTEROP_029-032 + REQ_INTEROP_096-097 verified

Checklist

  • Breaking changes are clearly described (and announced in docs / changelog if needed)
  • Tests were added or updated if needed
  • Docs were updated if behavior or public API changed

@bburda bburda self-assigned this Mar 19, 2026
@bburda bburda marked this pull request as ready for review March 19, 2026 18:23
Copilot AI review requested due to automatic review settings March 19, 2026 18:23
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an event-based “Triggers” subsystem to ros2_medkit_gateway, enabling condition-driven notifications (primarily via SSE) when observed resources change, with optional persistence via SQLite and plugin-extensible condition evaluators.

Changes:

  • Introduces core trigger infrastructure: ResourceChangeNotifier, condition evaluators/registry, TriggerManager, and SQLite-backed persistence.
  • Adds REST endpoints for trigger CRUD + SSE event streaming and wires triggers into discovery/capabilities and gateway lifecycle.
  • Expands unit/integration test coverage and updates docs/config to describe trigger behavior and parameters.

Reviewed changes

Copilot reviewed 64 out of 64 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/ros2_medkit_gateway/src/gateway_node.cpp Creates/wires trigger subsystem, notifier, store, subscribers; adds params and shutdown sequencing.
src/ros2_medkit_gateway/include/ros2_medkit_gateway/gateway_node.hpp Exposes TriggerManager/ConditionRegistry/ResourceChangeNotifier accessors and members.
src/ros2_medkit_gateway/src/http/rest_server.cpp Registers trigger routes for entities; adds handler wiring hook.
src/ros2_medkit_gateway/include/ros2_medkit_gateway/http/rest_server.hpp Adds set_trigger_handlers() and stores TriggerHandlers instance.
src/ros2_medkit_gateway/src/http/handlers/trigger_handlers.cpp Implements trigger CRUD + SSE stream and resource URI parsing/validation.
src/ros2_medkit_gateway/include/ros2_medkit_gateway/http/handlers/trigger_handlers.hpp Declares TriggerHandlers public API and helpers.
src/ros2_medkit_gateway/src/resource_change_notifier.cpp Implements async fan-out hub for resource change events (worker thread + filters).
src/ros2_medkit_gateway/include/ros2_medkit_gateway/resource_change_notifier.hpp Declares notifier types (ResourceChange, filters, subscribe/notify/shutdown).
src/ros2_medkit_gateway/include/ros2_medkit_gateway/condition_evaluator.hpp Adds built-in condition evaluators + thread-safe registry for plugin extensibility.
src/ros2_medkit_gateway/src/trigger_manager.cpp Implements trigger lifecycle, condition evaluation, indexing, SSE wakeups, persistence integration.
src/ros2_medkit_gateway/include/ros2_medkit_gateway/trigger_manager.hpp Declares TriggerManager API, state model, and configuration.
src/ros2_medkit_gateway/include/ros2_medkit_gateway/trigger_store.hpp Introduces TriggerStore abstraction and TriggerInfo persisted model.
src/ros2_medkit_gateway/include/ros2_medkit_gateway/sqlite_trigger_store.hpp Declares SQLite implementation for trigger persistence/state.
src/ros2_medkit_gateway/src/trigger_topic_subscriber.cpp Adds generic topic subscriber with retry timer for late type resolution.
src/ros2_medkit_gateway/include/ros2_medkit_gateway/trigger_topic_subscriber.hpp Declares topic subscriber API and retry/pending behavior.
src/ros2_medkit_gateway/src/trigger_fault_subscriber.cpp Bridges /fault_manager/events into notifier events for trigger evaluation.
src/ros2_medkit_gateway/include/ros2_medkit_gateway/trigger_fault_subscriber.hpp Declares fault-event subscriber bridge.
src/ros2_medkit_gateway/include/ros2_medkit_gateway/trigger_transport_provider.hpp Adds plugin interface for alternative trigger event delivery protocols.
src/ros2_medkit_gateway/src/updates/update_manager.cpp Emits update status changes to notifier for triggers.
src/ros2_medkit_gateway/include/ros2_medkit_gateway/updates/update_manager.hpp Adds notifier integration points for update status notifications.
src/ros2_medkit_gateway/src/operation_manager.cpp Emits action status changes to notifier; threads entity_id through goal tracking.
src/ros2_medkit_gateway/include/ros2_medkit_gateway/operation_manager.hpp Adds notifier support and stores entity_id for operation trigger notifications.
src/ros2_medkit_gateway/src/http/handlers/operation_handlers.cpp Passes entity_id into OperationManager action goal creation for trigger notifications.
src/ros2_medkit_gateway/src/log_manager.cpp Adds programmatic add_log_entry() and emits log CREATED notifications.
src/ros2_medkit_gateway/include/ros2_medkit_gateway/log_manager.hpp Declares add_log_entry() + notifier wiring for log triggers.
src/ros2_medkit_gateway/src/plugins/plugin_context.cpp Exposes notifier + condition registry to plugins via PluginContext.
src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_context.hpp Adds trigger-related accessors (notifier/registry) to plugin API.
src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_types.hpp Bumps PLUGIN_API_VERSION for ABI change.
src/ros2_medkit_gateway/src/http/handlers/health_handlers.cpp Advertises trigger availability in root capabilities.
src/ros2_medkit_gateway/src/http/handlers/discovery_handlers.cpp Adds trigger links and TRIGGERS capability for entities.
src/ros2_medkit_gateway/include/ros2_medkit_gateway/http/handlers/capability_builder.hpp Adds TRIGGERS capability enum value.
src/ros2_medkit_gateway/src/http/handlers/capability_builder.cpp Maps TRIGGERS capability to "triggers" string.
src/ros2_medkit_gateway/include/ros2_medkit_gateway/http/handlers/handlers.hpp Adds TriggerHandlers include to handler aggregation header.
src/ros2_medkit_gateway/package.xml Adds SQLite system dependency.
src/ros2_medkit_gateway/CMakeLists.txt Finds/links SQLite3; adds trigger sources to gateway_lib; adds new gtests.
src/ros2_medkit_gateway/test/test_condition_evaluator.cpp Unit tests for built-in evaluators + registry + plugin extension path.
src/ros2_medkit_gateway/test/test_resource_change_notifier.cpp Unit tests for notifier filtering, async behavior, shutdown safety.
src/ros2_medkit_gateway/test/test_trigger_store.cpp Unit tests for SQLite trigger store CRUD + state persistence.
src/ros2_medkit_gateway/test/test_trigger_handlers.cpp Unit tests for resource URI parsing, JSON shape, SSE tracker, error schema.
src/ros2_medkit_gateway/test/test_log_manager.cpp Adds tests for add_log_entry() behavior and severity normalization.
src/ros2_medkit_gateway/test/test_graph_provider_plugin.cpp Updates FakePluginContext for new PluginContext virtuals.
src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/test/test_topic_beacon_plugin.cpp Updates MockPluginContext for new PluginContext virtuals.
src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/test/test_param_beacon_plugin.cpp Updates MockPluginContext for new PluginContext virtuals.
src/ros2_medkit_gateway/config/gateway_params.yaml Documents trigger parameters in default YAML config.
src/ros2_medkit_gateway/design/index.rst Documents trigger subsystem architecture and component diagram.
docs/config/server.rst Documents trigger config options and SSE limits including trigger streams.
docs/requirements/specs/subscriptions.rst Marks trigger requirements verified and adds REQ_INTEROP_096/097.
docs/requirements/specs/discovery.rst Marks REQ_INTEROP_002 verified.
docs/tutorials/index.rst Adds triggers tutorial entry.
README.md Adds “Triggers” to feature matrix.
src/ros2_medkit_integration_tests/test/features/test_triggers_updates.test.py Trigger CRUD integration tests for “updates” resources.
src/ros2_medkit_integration_tests/test/features/test_triggers_faults.test.py Trigger CRUD + SSE connectivity tests for faults resources.
src/ros2_medkit_integration_tests/test/features/test_triggers_logs.test.py Trigger CRUD + SSE connectivity tests for logs resources.
src/ros2_medkit_integration_tests/test/features/test_triggers_hierarchy.test.py Integration tests for hierarchy-scoped triggers (area/component/function).
src/ros2_medkit_integration_tests/test/features/test_triggers_late_publisher.test.py Integration test intended to cover late publisher retry subscription.
src/ros2_medkit_integration_tests/test/features/test_triggers_persistent.test.py Integration tests for persistent trigger restore using shared SQLite DB.
src/ros2_medkit_integration_tests/test/scenarios/test_scenario_ota_monitoring.test.py Scenario test for multi-trigger CRUD + SSE connectivity for OTA monitoring workflow.

Comment on lines +344 to +368
std::string path = "/tmp/test_trigger_store_persist.db";
std::remove(path.c_str());

// Scope 1: create and save
{
SqliteTriggerStore store(path);
ASSERT_TRUE(store.save(make_trigger("persist_001")).has_value());
ASSERT_TRUE(store.save_state("persist_001", json{{"val", 99}}).has_value());
}

// Scope 2: reopen and verify
{
SqliteTriggerStore store(path);
auto loaded = store.load_all();
ASSERT_TRUE(loaded.has_value());
ASSERT_EQ(loaded->size(), 1u);
EXPECT_EQ(loaded->at(0).id, "persist_001");

auto state = store.load_state("persist_001");
ASSERT_TRUE(state.has_value());
ASSERT_TRUE(state->has_value());
EXPECT_EQ(state->value(), json({{"val", 99}}));
}

std::remove(path.c_str());
Comment on lines +55 to +60
void UpdateManager::notify_status_change(const std::string & id, const UpdateStatusInfo & status) {
if (!notifier_) {
return;
}
notifier_->notify("updates", id, id, update_status_to_json(status));
}
Comment on lines +37 to +51
// Derive entity_id from reporting_sources (first source, if available)
std::string entity_id;
if (!msg->fault.reporting_sources.empty()) {
entity_id = msg->fault.reporting_sources[0];
}

// Map event_type to ChangeType
ChangeType change_type = ChangeType::UPDATED;
if (msg->event_type == "fault_confirmed") {
change_type = ChangeType::CREATED;
} else if (msg->event_type == "fault_cleared") {
change_type = ChangeType::DELETED;
}

notifier_.notify("faults", entity_id, msg->fault.fault_code, fault_json, change_type);
Comment on lines +217 to +227
auto result = trigger_mgr_.create(create_req);
if (!result) {
// Distinguish between validation errors (400) and capacity errors (503).
// NOTE: String matching on TriggerManager error messages - coupled to the exact
// error text in trigger_manager.cpp::create(). If those strings change, update here.
const auto & err = result.error();
if (err.find("Maximum trigger capacity") != std::string::npos) {
HandlerContext::send_error(res, 503, ERR_SERVICE_UNAVAILABLE, err);
} else {
HandlerContext::send_error(res, 400, ERR_INVALID_PARAMETER, err, {{"parameter", "trigger_condition"}});
}
Comment on lines +257 to +260
// Subscribe to topic for data triggers
if (topic_subscriber_ && req.collection == "data" && !req.resolved_topic_name.empty()) {
topic_subscriber_->subscribe(req.resolved_topic_name, req.resource_path, req.entity_id);
}
bburda added 25 commits March 19, 2026 19:39
…ators

Header-only ConditionEvaluator abstract base class with evaluate() and
validate_params() methods. Four SOVD-standard condition types:
OnChange, OnChangeTo, EnterRange, LeaveRange. Thread-safe
ConditionRegistry for built-in and plugin-registered evaluators.
40 unit tests covering all evaluators, registry, and plugin extension.
Range evaluators now return false for non-numeric values instead of
throwing. Also document validate_params() precondition on base class.
Central async notification hub for resource changes. Producers call
notify() to report changes (non-blocking, pushes to internal queue).
Observers register callbacks with collection/entity/resource filters.
Dedicated worker thread processes the queue and dispatches to matching
subscribers with exception isolation.
- Remove spurious mutable on sub_mutex_ (no const methods lock it)
- Fix fragile multi-subscriber test with atomic counter pattern
- Fix copyright to match existing convention (bburda not selfpatch)
- Guard condition_params parse against corrupt JSON (is_discarded check)
- Add column allowlist to update() preventing arbitrary column modification
- Add lifetime_sec assertion to UpdateFields test
- Add UpdateDisallowedColumn test
…rarchy matching

TriggerManager is the central coordinator for the triggers feature. It
integrates ConditionRegistry, ResourceChangeNotifier, and TriggerStore
to provide full trigger lifecycle management:

- CRUD operations with capacity enforcement and condition validation
- Async condition evaluation on resource change events via dispatch index
- Entity hierarchy matching (components/areas scope down to apps)
- SSE synchronization (wait_for_event/consume_pending_event pattern)
- Lifetime expiry with lazy termination
- Persistent trigger loading on restart
- EventEnvelope generation with ISO 8601 timestamps

Also relaxes ResourceChangeNotifier filter matching: empty collection
now acts as a catch-all (matching all collections), consistent with the
existing empty entity_id and resource_path behavior.

29 unit tests covering CRUD, event evaluation, hierarchy matching,
SSE synchronization, lifetime expiry, and condition-specific behavior.
- Move store_.save() outside triggers_mutex_ in create() to avoid
  blocking dispatch during SQLite I/O. Persist before inserting into
  memory so failures leave no in-memory state.
- Clean up expired triggers from dispatch index and triggers_ map via
  new cleanup_expired_trigger() helper, preventing unbounded accumulation.
- Fix on_removed_ data race by copying callback under triggers_mutex_
  in remove() and guarding set_on_removed() with the same mutex.
- Replace sleep_for(100ms) with wait_for_event() + consume_pending_event()
  in tests that expect events; keep short sleeps only for negative tests
  (verifying no event fires) with explanatory comments.
- Reduce LifetimeExpiry test from 2s sleep to 1.1s.
- Reject non-positive lifetime in update() with validation error.
- Revert premature PLUGIN_API_VERSION bump (3->4); will bump in Task 11
  when PluginContext vtable actually changes.
- Add threading safety comments on TriggerState immutable fields.
Thin REST handler layer over TriggerManager following the
CyclicSubscriptionHandlers pattern. Implements CRUD (create, list, get,
update, delete) and SSE event streaming for triggers. Includes a local
parse_resource_uri that supports areas in addition to apps, components,
and functions.

22 unit tests cover resource URI parsing (including areas, path
traversal rejection), JSON serialization, error response format, and
SSE client tracker limits.
- Add explicit lifetime <= 0 validation in handle_update
- Document string-matching coupling for capacity error detection
- Add Cache-Control and X-Accel-Buffering SSE headers
- Document trigger_mgr_ lifetime requirement in SSE lambda
- Remove invalid @verifies tags (will be added in Task 16)
Add trigger route registration to RESTServer and integrate trigger
capability into the entity discovery system. Routes are registered
for all 4 entity types (areas, components, apps, functions) with
runtime guards that return 501 until TriggerManager is wired up
by GatewayNode (Task 7). Adds set_trigger_handlers() method for
deferred handler initialization.
…atewayNode

Initialize trigger infrastructure in GatewayNode: ResourceChangeNotifier,
ConditionRegistry (4 built-in evaluators), SqliteTriggerStore, and
TriggerManager. Add triggers configuration section to gateway_params.yaml
with enable flag, max triggers, restart behavior, and storage path.
Wire trigger handlers into REST server and add proper shutdown ordering.
…text

- Add TriggerTransportProvider abstract interface (trigger_transport_provider.hpp)
  with TriggerEventDelivery struct for delivering trigger events over custom protocols
- Extend PluginContext with four new pure virtual methods:
  get_resource_change_notifier(), get_condition_registry(),
  set_trigger_store(), register_trigger_transport()
- Implement new methods in GatewayPluginContext: getters delegate to GatewayNode,
  setters store override store and transport providers as members
- Update FakePluginContext in tests to implement the new interface methods
- Bump PLUGIN_API_VERSION from 3 to 4
…, and hierarchy triggers

Five new integration test files covering the full trigger lifecycle:

- test_triggers_data: CRUD + SSE event delivery for data topics (20 tests)
- test_triggers_faults: CRUD + SSE connectivity for fault triggers (8 tests)
- test_triggers_updates: CRUD + error handling for update triggers (12 tests)
- test_triggers_logs: CRUD + SSE connectivity for log triggers (9 tests)
- test_triggers_hierarchy: entity scoping across components, areas, functions (7 tests)

Also fixes beacon plugin MockPluginContext to implement new PluginContext
pure virtual methods from Task 11 (get_resource_change_notifier,
get_condition_registry, set_trigger_store, register_trigger_transport).
Add requirements traceability for trigger endpoints:
- Add REQ_INTEROP_096 (GET single trigger) and REQ_INTEROP_097 (GET
  trigger events SSE) to subscriptions.rst as new verified requirements
- Update REQ_INTEROP_029 through REQ_INTEROP_032 from open to verified
- Add @verifies tags to test_trigger_handlers.cpp unit tests
- Add @verifies tags to all 5 trigger integration test files
- REQ_INTEROP_002 auto-updated to verified by generate_verification.py
…subscription

Data triggers never fire end-to-end because parse_resource_uri() captures
resource_path as a URI segment (e.g. "/temperature") while
TriggerTopicSubscriber emits the full ROS 2 topic name
(e.g. "/sensor/temperature") as resource_path in notifications. The
comparison in on_resource_change() never matches.

Fix by resolving the resource_path to a full ROS 2 topic name at trigger
creation time in trigger_handlers.cpp using the entity's cached topic
data. TriggerTopicSubscriber now uses the original resource_path (not
the topic name) in notifications, aligning both sides of the match.

Also fix multi-entity topic subscription: TriggerTopicSubscriber now
stores a set of entity_ids per topic instead of a single entity_id, and
emits one notification per entity. unsubscribe() removes individual
entities and only destroys the ROS 2 subscription when the set is empty.
- Remap 16 @verifies tags from undefined REQ_TRIGGER_001/002/003 to
  existing requirement IDs (REQ_INTEROP_029-032, 096, 097)
- Add Triggers section to docs/config/server.rst documenting all
  trigger configuration parameters
- Update SSE max_clients description to mention trigger event streams
- Add Triggers row to README.md features table
- Fix log_settings docs in rest.rst to match implementation (severity
  and marker fields, not severity_filter and max_entries)
…ix multishot default

- Validate JSON Pointer syntax and length (max 1024) for the path field (I19)
- Reject unsupported protocols; only 'sse' is accepted (I20)
- Reject unknown collections; allow x-* vendor extensions (I21)
- Fix TriggerInfo::multishot default from true to false to match API contract (I3)
- Add unit tests: InvalidJsonPointer_Returns400, PathTooLong_Returns400,
  UnsupportedProtocol_Returns400, UnknownCollection_Returns400,
  VendorExtensionCollection_Accepted
…g_settings in response

- I24: Add SSE id: field to every event frame using per-trigger event_counter (atomic uint64_t in TriggerState), enabling client reconnection with Last-Event-ID
- I25: Add millisecond precision to to_iso8601() - formats as .NNN before trailing Z
- I26: Replace hardcoded {"triggers", true} with dynamic check via get_trigger_manager() != nullptr, consistent with updates capability
- I27: Include log_settings in trigger_to_json() when set, so clients can inspect what was configured
bburda added 6 commits March 19, 2026 19:44
…ent load, log entry

- I14: EnterRangeEvaluator/LeaveRangeEvaluator non-numeric guard tests (string,
  boolean previous/current values must return false without exception)
- I11: load_persistent_triggers() tests using SqliteTriggerStore + plain
  TriggerManager construction (restore/reset behavior, expired trigger
  TERMINATED in store, ID collision avoidance after high-ID restore)
- I12: LogManager::add_log_entry() tests using existing LogManagerBufferTest
  fixture (entry retrievable, invalid severity fallback to INFO, metadata
  suffix appended, empty metadata produces clean message)
- I13: TriggerFaultSubscriber event mapping requires rclcpp node and live ROS 2
  topic subscriptions; mapping logic is covered indirectly by integration tests
…ial prerequisites

- Add @verifies REQ_INTEROP_029 to condition validator and trigger store tests
- Add @verifies REQ_INTEROP_097 to resource change notifier tests
- Replace C++ constant names (ERR_RESOURCE_NOT_FOUND) with wire-format codes
  (resource-not-found) in trigger section of rest.rst
- Add prerequisite note for Scenario 2 explaining how to find the engine component
- Replace /path/to/demo_nodes_manifest.yaml placeholder with actual ros2 pkg path
- Add SOVD compliance note about script executions and locks not yet supported
  as observable trigger resources
When a trigger is created on a data topic whose publisher hasn't started
yet, get_topic_names_and_types() returns empty and the subscription was
silently skipped. Now unresolved topics are queued as pending and retried
every 5 seconds via a wall timer, with a 60-second timeout.

- Add PendingSubscription struct to track topics awaiting type resolution
- Extract create_subscription_internal() for shared use by subscribe()
  and retry_pending_subscriptions()
- Update subscribe() to queue pending instead of silently skipping
- Update unsubscribe() to also check and clean up pending entries
- Update shutdown() to cancel retry timer and clear pending map
- Add integration test for late publisher scenario
When a ROS 2 node disappears, its triggers remained active in
TriggerManager, wasting capacity slots and never receiving events.

Add EntityExistsFn and sweep_orphaned_triggers() to TriggerManager
with a two-phase locking approach to avoid deadlock. Wire into
GatewayNode's periodic refresh timer so orphaned triggers are
cleaned up after each discovery cache refresh.
@bburda bburda force-pushed the feat/triggers-203 branch from e7ae8fd to 5d74077 Compare March 19, 2026 19:09
bburda added 9 commits March 19, 2026 21:30
- Fix ROS_DOMAIN_ID=70 collision between test_graph_provider_plugin and
  test_lock_handlers (moved lock handlers to 77)
- Increase persistent trigger test timeouts to 60s for CI robustness
- Switch hierarchy diagnostics test from manifest_only to hybrid mode
  (manifest_only doesn't link runtime data topics)
- Increase hierarchy data topic wait from 15s to 30s
…start

The dual-gateway test was failing because both gateways started
simultaneously, causing SQLite WAL lock contention and the secondary
loading from an empty DB. Fixed by:
- Starting primary first, demo nodes after 2s
- Delaying secondary by 25s so tests 01-02 create triggers first
- Secondary then starts and loads persistent triggers from shared DB
- Removed cross-gateway delete verification (no real-time sync)
- Reformat trigger_topic_subscriber.cpp with ament_clang_format
- Fix MultishotRapidEvents test: wait+consume each event sequentially
  instead of firing all 3 and hoping the worker processes them all
  before the first wait_for_event (fails on Humble with slower GCC 11)
…geNotifier

ResourceChangeNotifier is pure C++ (no rclcpp dependency) but was using
std::cerr for exception logging. Replace with an optional ErrorLoggerFn
callback, wired to RCLCPP_WARN in GatewayNode. Follows the same pattern
as set_on_removed, set_entity_children_fn, set_entity_exists_fn.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Triggers - event-based conditional data delivery

2 participants