#4235·semaphore

Feature: Named project alerts instead of instance-wide fan-out

Author: EyJunge1Created Sep 12, 2026Updated Sep 12, 2026

Related to

Web-Frontend (what users interact with), Web-Backend (APIs), Service (scheduled tasks, alerts), Database

Impact

major improvement to user experience

Missing Feature

Alerting today is instance-wide: one Slack URL, one Teams URL, one Telegram chat in config.json, plus a project alert toggle and per-template suppress flags. Every job that is allowed to alert fans out to the same destinations. A new job that nobody configured still notifies everyone. There is no way to send “Ops Telegram” from one job and “Slack prod” from another, or to keep a nightly cron quiet while the same job still alerts when run by hand.

Related: #3387, #3588, #3724, https://github.com/semaphoreui/semaphore/discussions/2940. Maintainer note on #3588: notification refactor is on the roadmap.

Implementation

Treat an alert as one object: channel + destination + message. Users pick those objects on the job. Empty selection means no send. There is no runtime global default.

Alerting page (sidebar)

  • Create named entries: type (Telegram, Slack, email, Teams, Rocket.Chat, DingTalk, Gotify), destination (chat/thread, webhook URL, recipients), optional message body, enabled flag
  • Clone an entry to reuse a channel with a different text
  • Test one alert or all enabled alerts
  • No is_default. Instance config.json stays infrastructure only (SMTP, bot token)

Job (template)

  • Multiselect of alert entries + on success / on error
  • Message text is not chosen again on the job; it lives on the entry
  • Empty list → no alerts (manual and, if the schedule inherits, cron)

Schedule

  • Inherit from job (use the job list, including empty) or an own list
  • Own empty list → this cron is silent; manual runs still follow the job
  • Optional success/error overrides only when the schedule has its own list
Task
  └─ schedule has its own list? ──yes──► schedule list (may be empty)
                         │
                         no (inherit)
                         ▼
                      job list (may be empty)
                         │
                    empty? ──yes──► no send
                         │
                         no
                         ▼
                    send each selected entry

Examples:

  • Job with no alerts selected → nothing is sent (manual or inheriting cron)
  • Job selects “Ops Telegram” + “Slack prod” → both fire, each with its own body
  • Schedule overrides with “Nightly Telegram” → only that cron differs
  • Schedule own list empty → cron silent, manual still uses the job

Design

  • alert: id, project_id, name, type, enabled, destination fields, body. Unique (project_id, name). Delete is blocked while templates or schedules still reference it.
  • Job: alert_ids[] (empty = nothing), alert_on_success, alert_on_error. A PUT that omits alert_ids leaves bindings unchanged; [] clears them (same as variable groups).
  • Schedule: alert_mode inherit | ids; in ids mode alert_ids[] may be empty; alert_on_* nullable = inherit from the job.
  • Task snapshot: resolved IDs + events at task creation so every HA node sends the same set. Dedup via task__alert_send unique (task_id, alert_id, event).
  • Migration (one-shot, not a runtime default): turn instance/project channels into alert rows and attach those IDs to templates that already alerted (project.alert and not both suppress flags). Schedules stay inherit. After that, only the job list matters.
  • Security: webhook URLs must be http/https; loopback, link-local, and cloud metadata hosts are rejected; Gotify token is write-only in API JSON; webhook client does not follow redirects.
  • v1 scope: success/error only (no waiting_confirmation). Events apply to the whole selected set. User alert stays the email opt-in for “project users”.
  • Not v1: new providers, error-regex routing, recovered/duration events, integration bindings, extra-vars in the body.