The task was a Notion page: a table of brands, each with the date its banners were last refreshed, updated by hand whenever someone remembered. Nobody remembered. Creative that had been live for four months was still serving because the page said it was fine.
Instead of updating the page, we replaced it. A script runs every morning at six, asks the ad server one question, and posts to a Telegram channel only if something needs attention. It has no server, no database, and no cost beyond a free GitHub Actions minute a day.
How it decides
- One account-wide stats call: impressions by campaign and banner for the last seven days. A campaign with impressions is running. That is the whole definition of live.
- For each running campaign, read the creative date out of each live banner's name.
- If the newest live banner is older than 30 days, that campaign needs a refresh. Post it.
- If nothing is stale, post nothing. Silence means healthy. A daily all-clear message is noise people learn to ignore.
The part that did the real work
The date lives in the banner name, because the ad server's own created-date endpoints were not usable on this account: one reliably returned a server error, the other needed a permission scope the API client did not have. Banner names, on the other hand, are always readable. The catch is that a decade of trafficking by different people means the date is written a dozen ways: 16-02-2026, 6th Oct, 24th June, 23 Jul, a month name buried in the middle. The parser handles all of them and works out the year from the most recent past occurrence.
Around 30% of running campaigns still had no parseable date in their banner names, from an older naming convention. The checker lists those separately as "cannot assess" rather than pretending. That list became the argument for fixing the naming convention, which is the actual root cause.
Small gotchas that would have broken it
- The stats endpoint allows about ten calls a minute. The script waits seven seconds between calls rather than hoping.
- The campaign list endpoint occasionally returns a server error for no reason. Retry, don't reason about it.
- A manual dry-run workflow prints what it would post without posting. Every alerting script should have one, or you test it by annoying the channel.
What it replaced
The checker was written by an agent in about an hour, from a description of the Notion page and the rule. The API gotchas it ran into on the way (the 500s, the missing scope, the rate limit) are in its memory now, so the next monitor on that platform is faster to build. A page nobody updated, and a person who was meant to check it. Now a channel gets a message like "Brand X: newest live banner is 41 days old (300x250, 728x90 running)" and the creative refresh gets booked that day. Total build time was a few hours. It has run every morning since June.