Skip to main content

Ephemeral Alerts

Client-side alerts that run custom evaluation logic in your application. You define the condition as a function — the SDK handles timing, recovery, and notification delivery. Use ephemeral alerts when RelayX alerts built-in operators aren't enough.

Access via app.alert.

warning

Ephemeral alerts only evaluate data while your application is running. If your app stops, the evaluator stops — no alerts will fire until it's restarted.

API

createEphemeral()

Create a client-side alert with custom evaluation logic.

await app.alert.createEphemeral(params)

Returns the alert object with listen(), setEvaluator(), and stop() methods attached

const alert = await app.alert.createEphemeral({
name: "custom_check",
type: "EPHEMERAL",
config: {
topic: {
source: "TELEMETRY",
device_ident: "sensor_01",
last_token: "cpu_usage",
},
duration: 30,
recovery_duration: 15,
recovery_eval_type: "VALUE",
cooldown: 60,
},
notification_channel: [channelId],
});

Example response:

{
"name": "custom_check",
"type": "EPHEMERAL",
"config": {
"topic": {
"source": "TELEMETRY",
"device_ident": "sensor_01",
"last_token": "cpu_usage"
},
"duration": 30,
"recovery_duration": 15,
"recovery_eval_type": "VALUE",
"cooldown": 60
},
"notification_channel": ["69cb22a4ed4d9fe786145d01"]
}

The returned object also has listen(), setEvaluator(), and stop() methods attached.

Topic Config

FieldTypeDescription
sourcestring"TELEMETRY", "COMMAND", or "EVENT"
device_identstringDevice identifier, or "*" for all devices
last_tokenstringMetric/command/event name, or "*" for all

Recovery Eval Type

The recovery_eval_type field controls how the alert resolves after firing. Defaults to "VALUE".

ValueBehavior
"VALUE"The evaluator must continuously return false for the full recovery_duration before the alert resolves. Data must keep flowing — silence alone won't resolve it.
"TIMER"A countdown starts when the alert is in a fired or acknowledged state. If no data arrives for the recovery_duration, the alert resolves automatically. If data does arrive and the evaluator returns false for the recovery_duration, it also resolves normally.

Use "VALUE" when you need certainty that the condition has genuinely cleared — the evaluator must actively confirm it.

Use "TIMER" when silence itself means the problem is gone — for example, if a device stops sending error events, the alert should auto-resolve.

setEvaluator()

Define the custom evaluation function. Called on every incoming data point — return true to fire, false to clear. Only one evaluator can be attached per ephemeral alert — calling this again will throw an error.

alert.setEvaluator(fn)

The function receives a rolling state object. As new data arrives, the rolling state is updated — it always contains the latest value for each metric. Previous metrics are preserved even when new ones arrive.

For example, if temperature arrives at t=1 and humidity arrives at t=30, the rolling state at t=30 looks like:

{
"sensor_01": {
"temperature": { "value": 22.5, "timestamp": 1774690200000 },
"humidity": { "value": 61.3, "timestamp": 1774690229000 }
}
}

The evaluator is called every time a new data point arrives, with the full rolling state.

alert.setEvaluator((rollingState) => {
const temp = rollingState["sensor_01"]?.temperature?.value;
const humidity = rollingState["sensor_01"]?.humidity?.value;

// fire if both conditions are met
return temp > 80 && humidity > 70;
});

listen()

Start the evaluator and subscribe to alert state changes.

await alert.listen(callbacks)
CallbackDescription
onFire / on_fireAlert condition met, alert has fired
onResolved / on_resolvedAlert condition cleared, alert resolved
onAck / on_ackA device's alert incident was acknowledged
onError / on_errorEvaluator threw an error
await alert.listen({
onFire: (data) => console.log("FIRED:", data),
onResolved: (data) => console.log("RESOLVED:", data),
onAck: (data) => console.log("ACK:", data),
onError: (err) => console.error("Error:", err),
});

onFire continues to fire after acknowledgement

When an incident is acknowledged, the ephemeral engine stops dispatching internal notifications for as long as the incident stays acknowledged. However, while your evaluator is still returning a breach, the engine continues to publish fire events on the cooldown for the audit trail. Listener-mode subscribers receive those events and route them to onFire / on_fire, so the callback will keep being invoked after an ack.

The owner-mode evaluator suppresses its own local on_fire callback in the acknowledged state, but that suppression does not extend to listeners running in other processes.

Custom notifications

If your onFire callback dispatches its own notifications (Slack, PagerDuty, SMS, etc.), it will continue to fire after the incident has been acknowledged. The SDK does not silence custom notification logic for you. Track the acknowledged state in your callback and skip dispatch while the incident is in acknowledged. The onAck / on_ack callback gives you the transition you need.

stop()

Stop the evaluation engine and reset its state.

await alert.stop()

updateEphemeral()

Update an ephemeral alert's configuration.

await app.alert.updateEphemeral(params)
const updated = await app.alert.updateEphemeral({
id: alertId,
config: { duration: 60 },
});

Example response:

{
"name": "custom_check",
"type": "EPHEMERAL",
"config": {
"topic": {
"source": "TELEMETRY",
"device_ident": "sensor_01",
"last_token": "cpu_usage"
},
"duration": 60,
"recovery_duration": 15,
"recovery_eval_type": "VALUE",
"cooldown": 60
},
"notification_channel": ["69cb22a4ed4d9fe786145d01"]
}

The following operations work the same for both RelayX and ephemeral alerts. State changes (ack, mute, unmute) are globally synced — if one App SDK instance acknowledges or mutes an alert, all other instances see the change.

delete()

Permanently remove an alert.

await app.alert.delete(alertId)
await app.alert.delete(alertId);

list()

Retrieve all alerts.

await app.alert.list()
const alerts = await app.alert.list();

Example response:

[
{
"name": "high_temp",
"type": "THRESHOLD",
"metric": "temperature",
"config": {
"scope": { "type": "DEVICE", "value": "69ca39e4ed4d9fe786145b48" },
"operator": ">",
"value": 85,
"duration": 300,
"recovery_duration": 120
},
"notification_channel": ["69cb22a4ed4d9fe786145d01"]
}
]

get()

Retrieve a single alert by name.

await app.alert.get(name)
const alert = await app.alert.get("high_temp");

Example response:

{
"name": "high_temp",
"type": "THRESHOLD",
"metric": "temperature",
"config": {
"scope": { "type": "DEVICE", "value": "69ca39e4ed4d9fe786145b48" },
"operator": ">",
"value": 85,
"duration": 300,
"recovery_duration": 120,
"cooldown": 600
},
"notification_channel": ["69cb22a4ed4d9fe786145d01"]
}

history()

Fetch a chronological timeline of alert events (fire, resolved, ack) for a device, a rule, or the entire org. Each event carries an incident_id so you can group events by alerting episode. Works identically for ephemeral and RelayX alerts.

await app.alert.history(params)
ParameterTypeDescription
rule_typestring"DEVICE", "RULE", or "ORG"
device_identstringSingle-device shorthand. Required for "DEVICE" unless device_idents is set
device_identsstring[]Multi-device variant. Optional on every rule_type. Filters results to events from these devices
rule_idstringRequired when rule_type is "RULE". Optional on "DEVICE" and "ORG" (narrows the result)
rule_statesstring[](optional) Subset of ["fire", "resolved", "ack"]. Defaults to all three
incident_idstring(optional) Filter to a single incident's events
startstringStart time (ISO 8601, UTC)
endstringEnd time (ISO 8601, UTC). Must be after start

rule_type semantics:

rule_typeRequired companionReturns
"DEVICE"device_ident or device_identsEvery alert event for those devices
"RULE"rule_idEvery event for that rule. Optionally narrowed with device_idents
"ORG"(none)Every alert event in the org. Optionally narrowed with device_idents and/or rule_id

Returns { events: [...] } where each event has the shape:

FieldTypeDescription
statestring"fire", "resolved", or "ack"
valueanyThe rolling state captured at the event
timestampnumberUnix milliseconds
incident_idstring | nullServer-generated UUID for the alerting episode. Persists across re-fires/acks until resolved
rule_idstringThe rule that fired
device_idstringThe device the event applies to

Events are returned in chronological order across the whole window.

// Full timeline for a device
const { events } = await app.alert.history({
rule_type: "DEVICE",
device_ident: "sensor_01",
start: "2026-03-01T00:00:00.000Z",
end: "2026-03-25T00:00:00.000Z",
});

// Org-wide, every alert in the window. One round trip.
const { events } = await app.alert.history({
rule_type: "ORG",
start: "2026-03-01T00:00:00.000Z",
end: "2026-03-25T00:00:00.000Z",
});

// One incident's full timeline
const { events } = await app.alert.history({
rule_type: "RULE",
rule_id: "rule_abc123",
incident_id: "uuid-here",
start: "2026-03-01T00:00:00.000Z",
end: "2026-03-25T00:00:00.000Z",
});

Querying acknowledgements

Acknowledgement events are part of the same timeline. Pass rule_states: ["ack"] to query only acks.

Grouping events into incidents

Every event carries an incident_id. An incident opens on the normal → alerting transition (UUID generated server-side), persists across re-fires and acks, and closes on the return to normal. To present an incident-level UI, reduce events by incident_id.

History retention

Alert history retention depends on your plan. For example, the free plan retains 3 days of history. Queries outside your retention window will return empty results.

ack()

Acknowledge a specific device's alert.

await app.alert.ack(params)
ParameterTypeDescription
device_identstringThe device identifier
alert_idstringThe alert ID
acked_bystringWho is acknowledging (e.g., operator name)
ack_notesstring(optional) Notes about the acknowledgement
await app.alert.ack({
device_ident: "sensor_01",
alert_id: alertId,
acked_by: "operator_jane",
ack_notes: "Investigating issue",
});

mute()

Silence an alert so it stops delivering notifications.

await app.alert.mute(params)
ParameterTypeDescription
idstringThe alert ID
mute_config.typestring"FOREVER" or "TIME_BASED"
mute_config.mute_tillstring(TIME_BASED only) ISO 8601 timestamp to unmute at
Mute TypeBehavior
"FOREVER"The alert stays muted until explicitly unmuted via unmute(). The alert still evaluates but notifications are suppressed.
"TIME_BASED"The alert is muted until the specified mute_till timestamp, then automatically unmutes.
// mute forever
await app.alert.mute({
id: alertId,
mute_config: { type: "FOREVER" },
});

// mute until a specific time
await app.alert.mute({
id: alertId,
mute_config: {
type: "TIME_BASED",
mute_till: "2026-04-01T00:00:00.000Z",
},
});

unmute()

Re-enable a muted alert.

await app.alert.unmute(alertId)
await app.alert.unmute(alertId);