Skip to main content

Notification Engine

Receive events on a webhook, drop duplicates, keep only the critical and high severity ones, format a one-line message, and POST it to a notification endpoint, retrying when the endpoint fails.

Components: http_server input → dedupe (memory cache) → mapping (severity filter) → mapping (message format) → http_client output with retries.

Needs: an HTTP endpoint you control that accepts a JSON POST and answers 2xx. The job sends it the formatted message only.

Delivery was proved against a test receiver only

The body is {"text": ...} plus two fields, the shape Slack incoming webhooks accept, but no message was sent to Slack, email or any other real service. Point the job at your real receiver and check that messages arrive before you depend on it.

Complete job

notification-engine.yaml
name: notification-engine
type: pipeline
selector:
match_labels:
pipeline_role: notify
config:
input:
http_server:
address: "127.0.0.1:8088"
path: /events
allowed_verbs: [POST]
pipeline:
processors:
- dedupe:
cache: seen_events
key: '${! json("id") }'
- mapping: |
root = if ["critical", "high"].contains(this.severity) {
this
} else { deleted() }
- mapping: |
root.text = "[%s] %s: %s (event %s at %s)".format(
this.severity.uppercase(), this.source, this.message,
this.id, this.ts
)
root.event_id = this.id
root.severity = this.severity
cache_resources:
- label: seen_events
memory: {}
output:
http_client:
url: "https://RECEIVER_HOST/notify"
verb: POST
headers:
Content-Type: application/json
retries: 5
retry_period: 200ms
max_in_flight: 1

The selector sends the job to a node labelled pipeline_role: notify; set that label on your node, or change the selector to a label it already carries. With no matching node, the job is stored and never runs.

Replace https://RECEIVER_HOST/notify with your endpoint. The webhook listens on 127.0.0.1:8088 of the node that runs the job, so only processes on that node can reach it; change address to accept events from elsewhere.

Send it test events

Save these eight events on the node as events.json. Two are critical, one is high, one repeats an earlier id, and the rest are below the threshold:

events.json
[
{
"id": "evt-001",
"severity": "critical",
"source": "pump-4",
"message": "Discharge pressure above limit",
"ts": "2026-09-17T12:00:00Z"
},
{
"id": "evt-002",
"severity": "info",
"source": "pump-4",
"message": "Heartbeat",
"ts": "2026-09-17T12:00:05Z"
},
{
"id": "evt-003",
"severity": "high",
"source": "chiller-2",
"message": "Coolant temperature rising",
"ts": "2026-09-17T12:01:00Z"
},
{
"id": "evt-004",
"severity": "low",
"source": "gateway-1",
"message": "Clock skew 40ms",
"ts": "2026-09-17T12:01:30Z"
},
{
"id": "evt-001",
"severity": "critical",
"source": "pump-4",
"message": "Discharge pressure above limit",
"ts": "2026-09-17T12:00:00Z"
},
{
"id": "evt-005",
"severity": "warning",
"source": "chiller-2",
"message": "Filter change due",
"ts": "2026-09-17T12:02:00Z"
},
{
"id": "evt-006",
"severity": "critical",
"source": "gateway-1",
"message": "Uplink lost",
"ts": "2026-09-17T12:03:00Z"
},
{
"id": "evt-007",
"severity": "info",
"source": "gateway-1",
"message": "Uplink restored",
"ts": "2026-09-17T12:04:00Z"
}
]

Deploy the job, then post each event to the webhook from the node. This needs curl and jq:

expanso-cli job deploy notification-engine.yaml
jq -c '.[]' events.json | while IFS= read -r ev; do
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
-H 'Content-Type: application/json' --data "$ev" \
http://127.0.0.1:8088/events
done

Each post should print 200.

Check it

Your receiver should get exactly three messages. Each body is a JSON object with event_id, severity and text. The text values the test receiver recorded were:

  • [CRITICAL] pump-4: Discharge pressure above limit (event evt-001 at 2026-09-17T12:00:00Z)
  • [HIGH] chiller-2: Coolant temperature rising (event evt-003 at 2026-09-17T12:01:00Z)
  • [CRITICAL] gateway-1: Uplink lost (event evt-006 at 2026-09-17T12:03:00Z)

This job keeps running and listening until you stop it.

What the run proved

  • 8 events were accepted. Exactly the 3 critical or high events were delivered, and the repeated event was delivered once.
  • The receiver answered 503 to the first two attempts. Both were retried, for 5 attempts in total, and every message arrived.
  • The job was deployed through Expanso Cloud to a labelled node running expanso-edge v2.1.21. The receiver was a local test server on the same host.

Limits

  • Slack, email, Discord and PagerDuty were not contacted. There is no dedicated component for any of them; they are reached with http_client.
  • The memory dedupe cache is lost when the job restarts, so an event resent after a restart is delivered again. The RSS feed engine shows a file cache that survives restarts.