Alerts
Conditions on the goal's forecast, evaluated after every champion serve — 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.
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 — or a fleet metric (member_score, top_decile_entries, rank_jump, fleet_median_score). |
scope required | Which part of the horizon: any, last, min, max, or mean. Ignored by fleet metrics. |
operator required | gt, gte, lt, or lte. |
threshold required | A number (0–100 for probability — percent likelihood). |
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. 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" · "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"]
}
}metrics is the valid metric set for this goal's type, and has_forecast
tells you whether there's anything to evaluate against yet.
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.").
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
Evaluation happens after every champion serve. 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.

