Skip to content

Rule chains

Where the rules engine handles simple condition → action logic per point, rule chains are a graph of nodes that route, transform, enrich, and act on messages as they flow through. A chain is the place to build multi-step pipelines: filter, look up a related asset, gate on blockchain finality, create an alarm, and push to an external system — all with explicit branching, retries, and a dead-letter queue.

Rule chains are tenant-scoped and stored in Aerospike namespace rules, set chains (keyed {tenant_id}:{chain_id}), with immutable deployed versions in rules/chain_versions (keyed {tenant_id}:{chain_id}:{version}).

A rule chain version is a directed graph: nodes connected by links, with a designated root_node_id where messages enter. A version record carries chain_id, version, name, status, nodes, links, root_node_id, created_by, deployed_at, config_hash, and schema_version.

flowchart TD
  input["input:<br/>telemetry"] --> filter{"filter:<br/>CEL<br/>data.temperature > 80"}
  filter -->|true| finality["blockchain:<br/>require final proof"]
  filter -->|false| done([drop])
  finality -->|final| enrich["enrichment:<br/>get related asset"]
  finality -->|not final| wait([route to not_final])
  enrich --> alarm["action:<br/>create alarm"]
  alarm --> connector["connector:<br/>HTTP POST<br/>(SSRF-checked)"]
  connector -->|2xx| ok([success])
  connector -->|retries exhausted| dlq[("DLQ")]

The runtime supports these built-in node categories:

CategoryNodes
inputtelemetry, device event, alarm event, RPC response, schedule
filterCEL filter, message type switch, originator type switch, relation filter
transformscript transform, rename / copy / delete keys, JSON path, split array
enrichmentdevice attributes, customer attributes, related entity data, latest telemetry
actioncreate alarm, clear alarm, save telemetry, update attributes, send RPC, schedule notification
connectorHTTP, MQTT, Kafka, RabbitMQ, Google Pub/Sub, AWS SNS/SQS/Lambda, Redpanda, email
flowcheckpoint, delay, deduplication, retry, output, rule-chain input
blockchainrequire final proof, attach proof status, export proof bundle

A few worth calling out for operators:

  • Enrichment from related entities. If device D1 is related to asset A1 by located_at, a “get related asset” node attaches asset_id=A1 and configured asset fields to the message metadata — letting downstream nodes branch on asset context. See the entity model.
  • Blockchain finality filter. A require_final_proof node inspects the message’s proof state. If a point is only anchored (not yet final), the node delays or routes it to a not_final relation per its config; once the point reaches final the message continues. This is how a chain can guarantee it only acts on blockchain-finalized data.
  • Create alarm. Routes into the first-class alarm subsystem — see Alarms & notifications.

Editing happens on a draft; deploying produces an immutable version.

  1. Create / edit a draft. Creating a chain starts version 1 with status="draft" and publishes rulechain.created.T1.{chain_id}.

  2. Validate before deploy. Deployment runs graph validation first (see below).

  3. Deploy. On success the new version becomes active; the previously active version is retained for rollback. A rulechain.deployed.T1.{chain_id}.v{n} event is published.

Because deployed versions are immutable, you always know exactly which graph processed a given message — the version is recorded on DLQ records and debug traces.

A chain cannot be deployed unless its graph is structurally sound. Validation rejects, with INVALID_ARGUMENT and specific errors, problems such as:

  • A link pointing to a missing node (e.g. a link to N9 that does not exist) — the error identifies the missing node.
  • Cycles in the graph and unreachable nodes.

If validation fails, the previously deployed version stays active — a bad draft never takes a running chain down.

Every deployed version is retained, so rolling back is just re-activating an earlier version. Rolling chain C1 from active version 4 back to version 3 makes version 3 active again and publishes rulechain.rolled_back.T1.C1.v3.

Connector nodes reach out to external systems (HTTP endpoints, Kafka, MQTT, cloud queues, email). They MUST support bounded retries, timeouts, authentication, secret references, and SSRF-safe outbound networking.

  • Secret references. Credentials are referenced, not inlined — connector config holds a reference resolved at runtime, so secrets are never stored in the chain graph or included in export bundles.
  • SSRF policy. Outbound targets are validated. A connector aimed at an internal or metadata address — for example http://169.254.169.254/latest/meta-data — is rejected at validation time with INVALID_ARGUMENT, and an audit event rulechain.connector.rejected records reason="ssrf_blocked".

On success, an HTTP connector routes the message to its success relation on a 2xx response. On failure it retries within its bounded policy; if a connector (e.g. Kafka) exhausts its retries, the message goes to the dead-letter queue and a node failure metric increments with the node type.

Failed messages land in a tenant-scoped DLQ with enough context to inspect, replay, or discard them. A DLQ record holds dlq_id, tenant_id, chain_id, chain_version, node_id, message_payload, metadata, failure_reason, retry_count, first_failed_at, last_failed_at, and status.

  • On entry. A non-retryable failure (e.g. a validation error at node N1) creates a DLQ record and publishes rulechain.dlq.created.T1.{dlq_id}.
  • Replay. Replaying an open record re-runs the message — typically against the latest deployed version, which is the point of replay after you’ve fixed and redeployed a chain. The record’s status becomes replayed and an audit event rulechain.dlq.replayed is written.
  • Discard. Records you don’t intend to reprocess can be discarded.

To inspect a chain without permanently storing full payloads, start a debug session for a bounded time.

  • Starting a debug session (e.g. 10 minutes on chain C1) causes per-node trace records to be emitted to rulechain.debug.T1.C1. The UI shows, per node, its status, latency, input metadata, output metadata, and error details.
  • When the session expires, trace emission stops and runtime overhead returns to normal — debugging is opt-in and time-boxed, not an always-on cost.

Full payloads are not persisted unless you explicitly configure the session to do so.

The UI provides a graph editor with a node palette, per-node configuration panels, graph validation, deploy, rollback, the debug trace view, and DLQ replay. Clicking Deploy runs the same validation described above and, on success, publishes a new immutable version whose metadata is shown on the canvas.