ingestlayer/blog

all posts
Post#rant

Notifications as code: alert routing belongs in git.

The rules deciding what your team hears about production live in dashboards nobody reviews. Notifications as code puts them in git, behind a PR.

ben7 min read


Somewhere in your stack there is a rule that decides whether a human hears when payments start failing. It is probably not in your repo. It's a toggle in a dashboard, set months ago by someone who may not work there anymore, and the only way to find out what it does is to log in and look. Notifications as code is the argument that this rule is production config, and deserves to live where production config lives: in git.

Not a radical argument. Just one that stopped short of the place it matters most.

the config nobody can review

A dashboard change has no diff. Someone tightens a filter, mutes a channel, reroutes an alert, and there is no PR where a colleague could have said wait, that's the only alert we have on dunning. There's no blame when the postmortem asks why nobody was told, no revert when the change turns out wrong, no way to see what the rules were last March when the numbers looked strange. The change is applied the moment it's made, invisibly, to production.

And the knowledge rots. Why does #billing skip renewals? Why is the signup alert quiet on weekends? The answer lives in whoever clicked the toggle, which means the answer is archaeology: ask around, log in, squint at eight screens of settings, and hope staging was clicked the same way as prod. It never was.

infra already learned this lesson

Infrastructure went through this exact arc and came out the other side. Nobody serious edits production DNS in a web console anymore; it's Terraform, in a repo, behind review. Alerting on metrics made the same journey: Prometheus rules are files, monitors are code, and “alerts as code” is settled practice for CPU, latency, and error rates.

But it stopped at the metrics layer. Product events (the signup, the failed invoice, the job that died) got routed through clicky tools instead, because they didn't look like infrastructure. They are. The routing of those events decides what your team learns about production and when. That's as load-bearing as config gets, and it's the one kind still being edited live, by hand, with no witnesses.

notifications as code, concretely

The alternative is one file per pipeline, in the repo, stating the whole rule in one place: what comes in, what happens to it, where it lands, and under what conditions. Here's the one for failed payments, as the diagram or as the file. Flip between them.

representation

01source

sourcehttp.webhookWebhook
matchinvoice.payment_failed

02pipeline · 3 steps

  • 01CTLdedupekey $event.payload.customer · within 24h
  • 02ENRenrich.company$event.payload.customer_email → company
  • 03CTLthrottlemax 5 per 1h · key billing-alerts

03destinations · 2

  • toslackSlack
    channel#billing
    when$event.payload.attempt_count >= 3
  • towarehouse.pgPostgres
    tableevents.payment_failures

Everything a reviewer would want to interrogate is on the page. The dedupe means one story per customer, not one per retry. The throttle caps how hard the channel can be hit when a bad deploy fails everyone at once. The when keeps attempts one and two away from humans, because they usually recover on their own. And every attempt lands in Postgres, including the quiet ones. None of it is hiding in a modal.

the pr that turns down the noise

pipelines/payment-failures.yaml
# PR #482 · "billing alerts: one page per hour is plenty"
   - throttle:
       key: billing-alerts
-      max: 20
-      per: 5m
+      max: 5
+      per: 1h

This is what changing an alert rule should look like. The reviewer can see precisely what goes quiet, and can object before the channel goes silent instead of in the postmortem after. The commit message holds the reasoning. git blame answers “why five an hour?” forever, and git revert un-mutes the channel in one command when the quiet turns out to be a mistake.

There are quieter wins too. The PR that adds a new event can carry its routing in the same diff, so nothing ships unheard. Staging and prod stop drifting, because they're the same file. And the new teammate learns what the team gets told about production by reading a directory, not by shadowing whoever holds the dashboard password.

how ingestlayer does this

In ingestlayer, every pipeline is exactly this file: YAML in your repo, checked in next to the code that emits the events. dedupe, throttle, classify, and the rest are typed actions, so the review is of a real schema, not free-form config. The noise moves and the fan-out you'd design in the file are the same ones the dashboard-shaped tools bury in settings; here they just have a diff.

Put your first pipeline in your repo. The quickstart is a YAML file and one webhook away.


Read next

Summarize webhook events with an LLM before they hit Slack.

Raw payloads make unreadable alerts. Summarize webhook events with an LLM mid-pipe: Slack gets one human sentence, Postgres keeps the full payload.

← back to all posts