#Incident Protocol Specification

Version 1.0 — 2026-07-10

A shared contract for publishing machine incidents over MQTT. When a rule engine, threshold monitor, or watchdog detects a condition that requires attention, it publishes an incident to a single well-known topic. A forwarding agent subscribes to that topic and routes incidents to PagerDuty, email, Slack, or any external notification system.

This protocol is deliberately narrow in scope: it covers alerting only — conditions that a human or an automated responder needs to act on. It is not a general-purpose event bus. Both Edge Hub services — tsend2mqtt (PLC) and iolinkmaster2mqtt (IO-Link) — emit incidents in this format from the shared rules engine.

#Terminology

Term Meaning
Incident A condition that requires attention. Has a lifecycle: trigger → (active) → resolve.
Producer Any agent that detects conditions and publishes incidents (PLC gateway, IO-Link agent, watchdog).
Forwarder A single agent that subscribes to incidents/, deduplicates, and routes to external systems (PagerDuty, Slack, etc.).
Source The producer's identity, matching its id in the discovery protocol.
Dedup key Groups related triggers into one logical incident. Same key = same incident.

#Topic

incidents/

One topic. All producers publish here. The forwarder subscribes here. No per-source or per-severity topic splitting — filtering happens in the payload.

  • QoS: 1 (at least once delivery)
  • Retained: no (incidents are point-in-time notifications)

#Payload

Every message on incidents/ MUST be a JSON object with exactly the fields below.

#Trigger

Published when a condition is first detected.

{
  "action":    "trigger",
  "dedup_key": "plc1-Bool137",
  "source":    "plc1",
  "severity":  "critical",
  "summary":   "PLC alarm Bool137 active",
  "time":      "2026-07-10T14:30:00.123Z",
  "data":      {"tag": "Bool137", "value": true, "rule": "alarm-camera"}
}

#Resolve

Published when the condition clears. Must use the same dedup_key as the original trigger. severity and summary are omitted — the forwarder matches by dedup_key.

{
  "action":    "resolve",
  "dedup_key": "plc1-Bool137",
  "source":    "plc1",
  "time":      "2026-07-10T14:30:42.456Z",
  "data":      {"tag": "Bool137"}
}

#Field definitions

Field Type Required Description
action string always "trigger" or "resolve".
dedup_key string always Unique identifier for this incident. Same key = same incident. Format: {source}-{condition_id}. Used by forwarders to group triggers and match resolves.
source string always The id field from the producer's discovery entry. Links to the schema at discovery/{source}/{source}.
severity string trigger only One of: critical, error, warning, info. Maps directly to PagerDuty severity levels.
summary string trigger only Human-readable one-line description. Appears in push notifications and SMS. Keep under 120 characters.
time string always ISO 8601 timestamp, UTC, millisecond precision. When the condition was detected, not when the message was published.
data object always Machine-readable context. Structure is producer-specific. May be {}.

No additional fields. Producers MUST NOT add fields outside those defined above. All incident-specific context goes in data. This keeps the envelope stable and parseable by any forwarder without source-specific knowledge. data is always an object — never a scalar or array.

#Severity levels

Severity Meaning Typical response
critical Immediate action required. Production stopped, safety risk, equipment damage. Page on-call, wake people up.
error Something is broken but not immediately dangerous. Degraded operation. Notify team channel, create ticket.
warning Approaching a limit. Action needed soon but not urgently. Dashboard highlight, daily digest.
info Notable occurrence, no action required. Audit trail. Log only.

#Dedup key design

The dedup_key determines incident identity. Two messages with the same dedup_key refer to the same incident.

Format: {source}-{condition_id} where condition_id is stable across triggers — typically the rule name or the tag name that caused the incident.

Examples:

  • plc1-Bool137 — alarm on a specific PLC tag
  • plc1-alarm-camera — alarm from a specific rule
  • iolink_vib_pump_1-vrms_x_high — vibration threshold on a specific sensor

Rules:

  • A trigger with a dedup_key that is already active (previously triggered, not yet resolved) is a duplicate — the forwarder SHOULD suppress it or update the existing incident.
  • A resolve with a dedup_key that is not active is a no-op — the forwarder SHOULD ignore it.
  • Producers MUST send resolve when the condition clears. Stale incidents without a resolve are the producer's bug.

#Edge detection and lifecycle

The rule engine tracks the boolean result of each rule's condition across frames. This drives the trigger/resolve lifecycle:

Transition incidents/
false → true (rising edge) Publish trigger
true → true (no change) Nothing
true → false (falling edge) Publish resolve
false → false (no change) Nothing

The resolve fires automatically when the condition clears — the producer does not need a separate resolve rule. Cooldown applies only to the rising edge (trigger). The falling edge (resolve) is never suppressed.

#Producer requirements

  1. Publish a retained discovery entry at startup.
  2. When a rule fires: execute the action first, then publish the incident.
  3. Publish incidents to incidents/ with QoS 1, not retained.
  4. Use a stable dedup_key per condition — the same condition re-triggering must use the same key.
  5. Always send a resolve when the triggering condition clears.
  6. Set time to the time the condition was detected (e.g., the PLC frame timestamp), not wall clock.
  7. Apply cooldown/debounce before publishing. A condition that bounces at 1 kHz must not produce 1000 incidents per second. Recommended: rising-edge detection + minimum cooldown of 5 seconds.
  8. Keep summary human-readable and under 120 characters.

#Forwarder requirements

  1. Subscribe to incidents/ at startup.
  2. Subscribe to discovery/# to build a source registry.
  3. Deduplicate by dedup_key: suppress duplicate triggers for already-active incidents.
  4. Match resolve messages to active incidents by dedup_key.
  5. Route based on severity (e.g., critical → PagerDuty page, warning → Slack).
  6. Handle unknown source values gracefully (log warning, still forward).
  7. Handle unknown severity values by treating them as error.
  8. Track incident duration (time between trigger and resolve) for metrics.

#Mapping to PagerDuty Events API v2

For forwarders that route to PagerDuty, the mapping is direct — no transformation needed:

Incident field PagerDuty field
action event_action (trigger / resolve)
dedup_key dedup_key
severity payload.severity
summary payload.summary
time payload.timestamp
source payload.source
data payload.custom_details

#Wire compatibility checklist

Use this to verify your implementation:

  • Incidents published to exactly incidents/ (not incidents/source or sub-topics)
  • QoS 1, not retained
  • action is "trigger" or "resolve"
  • dedup_key is stable per condition, format {source}-{condition_id}
  • source matches the id in your discovery entry
  • severity is one of: critical, error, warning, info (trigger only)
  • summary is human-readable, under 120 characters (trigger only)
  • time is ISO 8601 UTC with millisecond precision
  • data is always an object (never a scalar or array)
  • Discovery entry published before first incident
  • Every trigger is eventually followed by a resolve with the same dedup_key
  • Cooldown/debounce applied — no burst of triggers for a bouncing condition