ArgoCD Notifications Deep Dive - Triggers, Templates, and Subscriptions
How the ArgoCD Notifications engine's trigger/template model works, and configuring per-application subscriptions to Slack, email, and webhooks
Introduction
The argocd-monitoring guide on this site covers wiring up a couple of notification channels quickly. This one covers the actual model underneath: triggers (conditions evaluated against Application state), templates (the message content), and subscriptions (which triggers send to which destination for which Application) - understanding the three lets you build genuinely custom notification logic instead of copy-pasting the built-in catalog.
The Three Pieces
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-notifications-cm
namespace: argocd
data:
# 1. Service: where a notification can be sent
service.slack: |
token: $slack-token
# 2. Template: what the notification says
template.app-sync-succeeded: |
message: |
Application {{.app.metadata.name}} is now running new version.
Sync status: {{.app.status.sync.status}}
slack:
attachments: |
[{
"title": "{{.app.metadata.name}}",
"color": "#18be52"
}]
# 3. Trigger: when to fire
trigger.on-sync-succeeded: |
- when: app.status.operationState.phase in ['Succeeded']
send: [app-sync-succeeded]
apiVersion: v1
kind: Secret
metadata:
name: argocd-notifications-secret
namespace: argocd
stringData:
slack-token: xoxb-your-actual-bot-token
Credentials always go in argocd-notifications-secret, referenced from argocd-notifications-cm with a $ prefix ($slack-token) - the ConfigMap itself is safe to check into Git as part of the GitOps-managed ArgoCD install; the Secret is not.
Subscribing an Application
A trigger existing in the ConfigMap doesn’t send anything by itself - an Application (or AppProject, for a subscription shared across every app in the project) has to subscribe to it via annotation:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: web-app
annotations:
notifications.argoproj.io/subscribe.on-sync-succeeded.slack: platform-alerts
notifications.argoproj.io/subscribe.on-sync-failed.slack: platform-alerts
notifications.argoproj.io/subscribe.on-sync-failed.email: oncall@example.com
spec:
# ...
The annotation format is notifications.argoproj.io/subscribe.<trigger-name>.<service-name>: <destination> - on-sync-succeeded fires to the platform-alerts Slack channel, on-sync-failed fires to both Slack and a specific email address. This is deliberately self-service: a team owning an Application can add their own subscription annotations without touching the shared argocd-notifications-cm, as long as the trigger and service they reference already exist there.
The Built-In Catalog
Installing the notifications catalog (kubectl apply against the catalog’s install manifest, or as part of the ArgoCD Helm chart with notifications.argocdUrl set) pre-populates a standard set of triggers you don’t have to write yourself: on-deployed, on-health-degraded, on-sync-failed, on-sync-running, on-sync-status-unknown, on-sync-succeeded - covering the common health/sync-state transitions most teams actually want alerted on, with matching Slack/Teams/email templates already written.
Writing a Custom Trigger
data:
trigger.on-deployed-stale: |
- when: app.status.sync.status == 'Synced' and app.status.operationState.finishedAt != nil and time.Now().Sub(time.Parse(app.status.operationState.finishedAt)) > time.Duration(24 * time.Hour)
send: [app-stale-warning]
template.app-stale-warning: |
message: |
{{.app.metadata.name}} hasn't synced in over 24 hours.
Trigger when conditions are expr expressions evaluated against the full Application object (available as app) - anything in app.status/app.spec is fair game, letting you build conditions the built-in catalog doesn’t cover, like this one flagging an app that’s synced but hasn’t actually run a sync operation recently.
Multiple Channels From One Trigger
data:
service.webhook.internal-audit-log: |
url: https://audit.internal.example.com/webhook
headers:
- name: Content-Type
value: application/json
metadata:
annotations:
notifications.argoproj.io/subscribe.on-sync-succeeded.webhook: internal-audit-log
Any number of service.<type>.<name> blocks can exist side by side - Slack, email, Microsoft Teams, PagerDuty, and generic webhooks are all supported service types, and a single trigger can be subscribed to several simultaneously (as the two on-sync-failed subscriptions above show), so a sync failure can page on-call and post to Slack and hit an internal audit webhook from the same trigger firing once.
Best Practices
- Subscribe at the
AppProjectlevel for org-wide alerting, and let individualApplicationannotations add team-specific channels on top - avoids every team having to remember to wire up the baseline alerts themselves. - Keep the Secret and ConfigMap changes in separate PRs/reviews where practical - the ConfigMap (templates, triggers) is low-risk to review casually; the Secret (bot tokens, webhook URLs) deserves tighter scrutiny.
- Test a new trigger’s
whenexpression against a real Application before rolling it out broadly - a subtly wrongexprcondition either never fires or fires on every reconcile, and both failure modes are easy to miss until someone notices the silence or the spam. - Use the built-in catalog as a starting point, not a ceiling - it covers the common cases, but the trigger/template model is fully generic, so don’t avoid a custom trigger just because it’s not in the catalog.
Conclusion
The notifications engine’s real design is triggers (state conditions) decoupled from templates (message content) decoupled from subscriptions (per-app opt-in) - once that split clicks, adding a new alert is usually a new trigger and a subscription annotation, not a redesign.