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.
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.
- JavaScript
- Python
await app.alert.createEphemeral(params)await app.alert.create_ephemeral(params)Returns the alert object with listen(), setEvaluator(), and stop() methods attached
- JavaScript
- Python
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],
});
alert = await app.alert.create_ephemeral({
"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": [channel_id],
})
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
| Field | Type | Description |
|---|---|---|
source | string | "TELEMETRY", "COMMAND", or "EVENT" |
device_ident | string | Device identifier, or "*" for all devices |
last_token | string | Metric/command/event name, or "*" for all |
Recovery Eval Type
The recovery_eval_type field controls how the alert resolves after firing. Defaults to "VALUE".
| Value | Behavior |
|---|---|
"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.
- JavaScript
- Python
alert.setEvaluator(fn)alert.set_evaluator(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.
- JavaScript
- Python
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;
});
def evaluator(rolling_state):
device = rolling_state.get("sensor_01", {})
temp = device.get("temperature", {}).get("value", 0)
humidity = device.get("humidity", {}).get("value", 0)
# fire if both conditions are met
return temp > 80 and humidity > 70
alert.set_evaluator(evaluator)
listen()
Start the evaluator and subscribe to alert state changes.
- JavaScript
- Python
await alert.listen(callbacks)await alert.listen(callbacks)| Callback | Description |
|---|---|
onFire / on_fire | Alert condition met, alert has fired |
onResolved / on_resolved | Alert condition cleared, alert resolved |
onAck / on_ack | A device's alert incident was acknowledged |
onError / on_error | Evaluator threw an error |
- JavaScript
- Python
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),
});
await alert.listen({
"on_fire": lambda data: print("FIRED:", data),
"on_resolved": lambda data: print("RESOLVED:", data),
"on_ack": lambda data: print("ACK:", data),
"on_error": lambda err: print("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.
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.
- JavaScript
- Python
await alert.stop()await alert.stop()updateEphemeral()
Update an ephemeral alert's configuration.
- JavaScript
- Python
await app.alert.updateEphemeral(params)await app.alert.update_ephemeral(params)- JavaScript
- Python
const updated = await app.alert.updateEphemeral({
id: alertId,
config: { duration: 60 },
});
updated = await app.alert.update_ephemeral({
"id": alert_id,
"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.
- JavaScript
- Python
await app.alert.delete(alertId)await app.alert.delete(alertId);
await app.alert.delete(alert_id)await app.alert.delete(alert_id)
list()
Retrieve all alerts.
- JavaScript
- Python
await app.alert.list()const alerts = await app.alert.list();
await app.alert.list()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.
- JavaScript
- Python
await app.alert.get(name)const alert = await app.alert.get("high_temp");
await app.alert.get(name)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.
- JavaScript
- Python
await app.alert.history(params)await app.alert.history(params)| Parameter | Type | Description |
|---|---|---|
rule_type | string | "DEVICE", "RULE", or "ORG" |
device_ident | string | Single-device shorthand. Required for "DEVICE" unless device_idents is set |
device_idents | string[] | Multi-device variant. Optional on every rule_type. Filters results to events from these devices |
rule_id | string | Required when rule_type is "RULE". Optional on "DEVICE" and "ORG" (narrows the result) |
rule_states | string[] | (optional) Subset of ["fire", "resolved", "ack"]. Defaults to all three |
incident_id | string | (optional) Filter to a single incident's events |
start | string | Start time (ISO 8601, UTC) |
end | string | End time (ISO 8601, UTC). Must be after start |
rule_type semantics:
rule_type | Required companion | Returns |
|---|---|---|
"DEVICE" | device_ident or device_idents | Every alert event for those devices |
"RULE" | rule_id | Every 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:
| Field | Type | Description |
|---|---|---|
state | string | "fire", "resolved", or "ack" |
value | any | The rolling state captured at the event |
timestamp | number | Unix milliseconds |
incident_id | string | null | Server-generated UUID for the alerting episode. Persists across re-fires/acks until resolved |
rule_id | string | The rule that fired |
device_id | string | The device the event applies to |
Events are returned in chronological order across the whole window.
- JavaScript
- Python
// 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",
});
# Full timeline for a device
result = 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",
})
events = result["events"]
# One incident's full timeline
result = 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.
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.
- JavaScript
- Python
await app.alert.ack(params)await app.alert.ack(params)| Parameter | Type | Description |
|---|---|---|
device_ident | string | The device identifier |
alert_id | string | The alert ID |
acked_by | string | Who is acknowledging (e.g., operator name) |
ack_notes | string | (optional) Notes about the acknowledgement |
- JavaScript
- Python
await app.alert.ack({
device_ident: "sensor_01",
alert_id: alertId,
acked_by: "operator_jane",
ack_notes: "Investigating issue",
});
await app.alert.ack({
"device_ident": "sensor_01",
"alert_id": alert_id,
"acked_by": "operator_jane",
"ack_notes": "Investigating issue",
})
mute()
Silence an alert so it stops delivering notifications.
- JavaScript
- Python
await app.alert.mute(params)await app.alert.mute(params)| Parameter | Type | Description |
|---|---|---|
id | string | The alert ID |
mute_config.type | string | "FOREVER" or "TIME_BASED" |
mute_config.mute_till | string | (TIME_BASED only) ISO 8601 timestamp to unmute at |
| Mute Type | Behavior |
|---|---|
"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. |
- JavaScript
- Python
// 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",
},
});
# mute forever
await app.alert.mute({
"id": alert_id,
"mute_config": {"type": "FOREVER"},
})
# mute until a specific time
await app.alert.mute({
"id": alert_id,
"mute_config": {
"type": "TIME_BASED",
"mute_till": "2026-04-01T00:00:00.000Z",
},
})
unmute()
Re-enable a muted alert.
- JavaScript
- Python
await app.alert.unmute(alertId)await app.alert.unmute(alertId);
await app.alert.unmute(alert_id)await app.alert.unmute(alert_id)