Alerts
Conditions on the goal's forecast — or on the event flow itself — with a live preview before you save.
An alert is a condition on the goal's forecast — warn me if the
forecast drops below 10,000 at any point over the horizon. Conditions are
evaluated after every champion forecast; when one triggers, it fires on the
goal's realtime channel as an alerts.evaluated event
and through your notification channels.
Alerts can also watch the event flow instead of the forecast — an important event just hit, the goal entered shock mode, a stream went quiet — see event rules.
The condition shape
{
"metric": "forecast_value",
"scope": "any",
"operator": "lt",
"threshold": 10000
}| Field | Meaning |
|---|---|
metric required | forecast_value, forecast_change_pct, uncertainty_pct, crossing, or probability — a fleet metric (member_score, top_decile_entries, rank_jump, fleet_median_score) — or an event rule (event_fired, shock_entered, event_quiet). |
scope required | Which part of the horizon: any, last, min, max, or mean. Ignored by fleet and event metrics. |
operator required | gt, gte, lt, or lte. Event rules fix this to gte. |
threshold required | A number (0–100 for probability — percent likelihood). |
stream | Event rules only — the event stream to watch. |
The metric set follows the goal type: forecast_value,
forecast_change_pct, uncertainty_pct, and crossing apply to level
goals (forecast_value, forecast_distribution); trend and anomaly
goals serve a probability, so only probability applies; and
fleet goals (rank_by_risk, rank_by_growth)
use the leaderboard-shaped set. The three
event rules apply to every goal type. Using the wrong
set returns a 400 that names the right one.
Create an alert
curl -X POST "$API_BASE/v1/goals/$GOAL_ID/alerts" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{
"name": "Sales dip warning",
"condition": { "metric": "forecast_value", "scope": "any", "operator": "lt", "threshold": 10000 },
"severity": "warning",
"cooldown_minutes": 240,
"channels": ["email", "slack"]
}'from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.goals.post_goals_by_goal_id_alerts(GOAL_ID, json={
"name": "Sales dip warning",
"condition": {
"metric": "forecast_value",
"scope": "any",
"operator": "lt",
"threshold": 10000,
},
"severity": "warning",
"cooldown_minutes": 240,
"channels": ["email", "slack"],
})
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.goals.postGoalsByGoalIdAlerts(GOAL_ID, {
json: {
name: "Sales dip warning",
condition: {
metric: "forecast_value",
scope: "any",
operator: "lt",
threshold: 10000,
},
severity: "warning",
cooldown_minutes: 240,
channels: ["email", "slack"],
},
});
{
"data": {
"alert": {
"alert_id": "alr_9F2C41D8A0B1C2D3E4F5A6",
"goal_id": "b7e9c2d4-…",
"name": "Sales dip warning",
"enabled": true,
"severity": "warning",
"condition": { "metric": "forecast_value", "scope": "any", "operator": "lt", "threshold": 10000.0 },
"cooldown_minutes": 240,
"channels": ["email", "slack"],
"recipients": [],
"created_at": "2026-07-15T15:00:00+00:00",
"last_evaluated_at": null,
"last_triggered_at": null,
"trigger_count": 0,
"last_result": null
}
}
}Request body
| Field | Meaning |
|---|---|
name required | Display name for the alert. |
condition required | The condition shape above. |
severity optional | warning (default) or critical. |
cooldown_minutes optional | Minimum time between firings. Defaults to 240; 0 re-alerts on every evaluation; capped at one week. |
channels optional | From in_app, email, websocket, slack, teams, pagerduty, webhook. null falls back to each recipient's notification preferences. |
recipients optional | User IDs. Adding people other than yourself requires an organization workspace, and they must be active members. |
Errors
| Status | Why | Example message |
|---|---|---|
400 | Missing or invalid field | "name is required" · "condition.metric must be one of forecast_value, forecast_change_pct, uncertainty_pct, crossing, probability, member_score, top_decile_entries, rank_jump, fleet_median_score, event_fired, shock_entered, event_quiet" · "severity must be one of warning, critical" |
400 | Metric doesn't fit the goal type | "condition.metric 'forecast_value' does not apply to a trend goal (it serves a probability); use one of: probability" |
400 | Cap reached | "Alert limit reached (25 per goal)" |
404 | Goal doesn't exist | "Goal not found" |
List alerts
curl "$API_BASE/v1/goals/$GOAL_ID/alerts" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID"from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.goals.get_goals_by_goal_id_alerts(GOAL_ID)
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.goals.getGoalsByGoalIdAlerts(GOAL_ID);
{
"data": {
"items": [
{
"alert_id": "alr_9F2C41D8A0B1C2D3E4F5A6",
"name": "Sales dip warning",
"enabled": true,
"trigger_count": 3,
"last_triggered_at": "2026-07-14T06:00:19+00:00",
"last_result": { "triggered": false, "observed": 12480.1, "at": "2026-07-15T06:00:12+00:00" },
"…": "…"
}
],
"has_forecast": true,
"goal_type": "forecast_value",
"metrics": ["forecast_value", "forecast_change_pct", "uncertainty_pct", "crossing", "event_fired", "shock_entered", "event_quiet"],
"event_streams": ["press_mentions", "promotions"]
}
}metrics is the valid metric set for this goal's type, has_forecast
tells you whether there's anything to evaluate against yet, and
event_streams lists the workspace's streams for
event rules.
Update & delete
Updates are partial — send only what changes:
curl -X PUT "$API_BASE/v1/goals/$GOAL_ID/alerts/$ALERT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{ "enabled": false }'from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.goals.put_goals_by_goal_id_alerts_by_alert_id(GOAL_ID, ALERT_ID, json={"enabled": False})
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.goals.putGoalsByGoalIdAlertsByAlertId(GOAL_ID, ALERT_ID, { json: { enabled: false } });
{ "data": { "alert": { "alert_id": "alr_9F2C…", "enabled": false, "…": "…" } } }curl -X DELETE "$API_BASE/v1/goals/$GOAL_ID/alerts/$ALERT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID"from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.goals.delete_goals_by_goal_id_alerts_by_alert_id(GOAL_ID, ALERT_ID)
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.goals.deleteGoalsByGoalIdAlertsByAlertId(GOAL_ID, ALERT_ID);
{ "data": { "deleted": "alr_9F2C41D8A0B1C2D3E4F5A6" } }Unknown alert IDs return 404 with "Alert not found" on update.
Preview a condition
Dry-run a condition against the champion's latest forecast — "would this trigger right now?" — before saving. Free:
curl -X POST "$API_BASE/v1/goals/$GOAL_ID/alerts/preview" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{ "condition": { "metric": "forecast_value", "scope": "any", "operator": "lt", "threshold": 10000 } }'from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.goals.post_goals_by_goal_id_alerts_preview(GOAL_ID, json={
"condition": {
"metric": "forecast_value",
"scope": "any",
"operator": "lt",
"threshold": 10000,
},
})
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.goals.postGoalsByGoalIdAlertsPreview(GOAL_ID, {
json: {
condition: {
metric: "forecast_value",
scope: "any",
operator: "lt",
threshold: 10000,
},
},
});
{
"data": {
"available": true,
"triggered": false,
"observed": 12480.1,
"detail": "Lowest forecast point is 12480.1 — above the 10000 threshold.",
"condition_label": "forecast below 10000 $ at any point over the horizon",
"forecast": {
"at": "2026-07-15T06:00:12+00:00",
"label": "daily_sales",
"anchor": 12841.5,
"path": [13102.4, 13350.9, "…"],
"p10": [12480.1, "…"],
"p90": [13724.9, "…"]
},
"unit": "$",
"goal_type": "forecast_value"
}
}If there's no forecast yet, you get 200 with available: false and a
reason ("No forecast yet — deploy a champion and let it serve once.").
Event rules
Three metrics watch the event flow rather than the forecast path. They use the same CRUD endpoints, severities, cooldowns, and channels, apply to every goal type, and are evaluated by their own hooks — the moment the thing happens, not on the next champion serve:
| Metric | Fires when | Evaluated | threshold | stream |
|---|---|---|---|---|
event_fired | An event on stream scores importance ≥ threshold | Seconds after the event is scored | 0–100 (percent importance) | Required |
shock_entered | The goal flips into shock mode | At shock entry | Unused (send 0) | Optional — restrict to one stream |
event_quiet | stream has had no event for ≥ threshold hours | Periodic sweep (a few minutes' granularity) | ≥ 1 (hours) | Required |
scope and operator are fixed (any / gte) — "at or above" is the
only reading that makes sense for all three. event_quiet is the
dead-man's-switch for pipelines that should produce events: a webhook
that went silent, a scraper that stopped scraping.
curl -X POST "$API_BASE/v1/goals/$GOAL_ID/alerts" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{
"name": "Major press event",
"condition": { "metric": "event_fired", "stream": "press_mentions", "operator": "gte", "scope": "any", "threshold": 70 },
"severity": "critical",
"channels": ["slack"]
}'from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.goals.post_goals_by_goal_id_alerts(GOAL_ID, json={
"name": "Major press event",
"condition": {
"metric": "event_fired",
"stream": "press_mentions",
"operator": "gte",
"scope": "any",
"threshold": 70,
},
"severity": "critical",
"channels": ["slack"],
})
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.goals.postGoalsByGoalIdAlerts(GOAL_ID, {
json: {
name: "Major press event",
condition: {
metric: "event_fired",
stream: "press_mentions",
operator: "gte",
scope: "any",
threshold: 70,
},
severity: "critical",
channels: ["slack"],
},
});
Validation errors name the problem: a missing stream returns
"condition.stream is required for event rules"; an event_fired
threshold outside 0–100 returns
"condition.threshold must be 0–100 (percent importance)"; an
event_quiet threshold under 1 returns
"condition.threshold must be at least 1 (hours without an event)".
Notes that differ from forecast alerts:
- Scheduled events don't fire
event_firedat recording time — the rule evaluates when the event actually occurs. - Preview works, but reports where the watched
quantity stands now instead of a forecast frame: the response carries
event_rule: trueand adetaillike"latest event scored importance 45% vs threshold 70%"or"goal is currently in 'normal' state"— noforecastobject. - The alert list response's
metricsarray always includes the three event metrics, and the list endpoint also returnsevent_streams— the workspace's stream keys — so builders can offer a picker.
Fleet goals
Fleet goals (rank_by_risk, rank_by_growth) use
the same CRUD endpoints, cooldowns, and channels, but their conditions read
the reduced leaderboard, evaluated once per completed scoring run
rather than per champion serve:
| Metric | Observed value | Threshold |
|---|---|---|
member_score | The extreme member's score in the operator's direction, as a percent (0.93 → 93) | Percent |
top_decile_entries | Members that entered the top decile this run (new to the board, or previously ranked below it) | Non-negative count |
rank_jump | The biggest single-run rank climb, in positions | Non-negative count |
fleet_median_score | The fleet's median score, as a percent — catches whole-population drift | Percent |
Notes that differ from single-signal alerts:
scopeis ignored — there is no horizon path to scope over; send"any".- A triggered condition delivers one digest naming the offending members (capped, worst first), not one notification per member. Cooldowns apply as usual, and delivery is idempotent per rule per run.
- Preview doesn't apply: fleet goals have no
champion forecast, so the endpoint returns
available: false. Thecondition_labelin the alert document still renders the human sentence (e.g. "any member's failure risk above 90%"). alerts.evaluatedstill fires on the goal's realtime channel after each run's evaluation, with the alert IDs that triggered.
curl -X POST "$API_BASE/v1/goals/$GOAL_ID/alerts" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{
"name": "Pump failure watch",
"condition": { "metric": "member_score", "scope": "any", "operator": "gte", "threshold": 90 },
"severity": "critical",
"channels": ["pagerduty"]
}'from predictai import PredictAI
client = PredictAI(token="pa_live_…", workspace_id="ws_…")
data = client.goals.post_goals_by_goal_id_alerts(GOAL_ID, json={
"name": "Pump failure watch",
"condition": {
"metric": "member_score",
"scope": "any",
"operator": "gte",
"threshold": 90,
},
"severity": "critical",
"channels": ["pagerduty"],
})
import { PredictAI } from "@predictai/sdk";
const client = new PredictAI({ token: "pa_live_…", workspaceId: "ws_…" });
const data = await client.goals.postGoalsByGoalIdAlerts(GOAL_ID, {
json: {
name: "Pump failure watch",
condition: {
metric: "member_score",
scope: "any",
operator: "gte",
threshold: 90,
},
severity: "critical",
channels: ["pagerduty"],
},
});
When an alert fires
Forecast conditions are evaluated after every champion serve; fleet conditions once per scoring run; event rules the moment their event happens. A triggered alert (outside its cooldown) does two things:
- publishes
alerts.evaluatedon the goal'sgoal:{goal_id}realtime channel, with the alert IDs that fired — so a dashboard or bot reacts within the same second; - delivers through the alert's
channels(or each recipient's preferences) via Notifications, with the human-readable condition label and the observed value.
The alert itself records last_evaluated_at, last_triggered_at,
trigger_count, and last_result, so the list view is an audit trail.

