Predict.aiDocs
Goals

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
}
FieldMeaning
metric requiredforecast_value, forecast_change_pct, uncertainty_pct, crossing, or probability — or a fleet metric (member_score, top_decile_entries, rank_jump, fleet_median_score).
scope requiredWhich part of the horizon: any, last, min, max, or mean. Ignored by fleet metrics.
operator requiredgt, gte, lt, or lte.
threshold requiredA 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"]
  }'
{
  "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

FieldMeaning
name requiredDisplay name for the alert.
condition requiredThe condition shape above.
severity optionalwarning (default) or critical.
cooldown_minutes optionalMinimum time between firings. Defaults to 240; 0 re-alerts on every evaluation; capped at one week.
channels optionalFrom in_app, email, websocket, slack, teams, pagerduty, webhook. null falls back to each recipient's notification preferences.
recipients optionalUser IDs. Adding people other than yourself requires an organization workspace, and they must be active members.

Errors

StatusWhyExample message
400Missing 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"
400Metric 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"
400Cap reached"Alert limit reached (25 per goal)"
404Goal 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"
{
  "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 }'
{ "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"
{ "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 } }'
{
  "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:

MetricObserved valueThreshold
member_scoreThe extreme member's score in the operator's direction, as a percent (0.9393)Percent
top_decile_entriesMembers that entered the top decile this run (new to the board, or previously ranked below it)Non-negative count
rank_jumpThe biggest single-run rank climb, in positionsNon-negative count
fleet_median_scoreThe fleet's median score, as a percent — catches whole-population driftPercent

Notes that differ from single-signal alerts:

  • scope is 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. The condition_label in the alert document still renders the human sentence (e.g. "any member's failure risk above 90%").
  • alerts.evaluated still 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"]
  }'

When an alert fires

Evaluation happens after every champion serve. A triggered alert (outside its cooldown) does two things:

  • publishes alerts.evaluated on the goal's goal:{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.

On this page