Automation.
Small serverless pipelines that remove repetitive work and run themselves. Pick a project below.
AIUB Notice Board
An AI-powered Telegram bot that watches the American International University-Bangladesh notices page and pings me the moment a new notice appears — already categorized and summarized in one line. It runs entirely on GitHub Actions, four times a day, with no server, no database and no API bill. $0 a month.
/ month
09 · 13 · 17 · 21 Dhaka
categories
free tier: 150
How it works.
Students and staff used to refresh the AIUB notices page hoping to catch
something important in time. The bot replaces that habit with a single
Telegram chat that only ever speaks when there's genuinely new news.
Each run is a short four-step pipeline, orchestrated by src/main.py.
-
1
Scrapesrc/scraper.py
Reads the AIUB notices listing as static HTML with BeautifulSoup, extracting each notice's title, date and detail-page URL.
-
2
Deduplicatestate/seen.json
Every scraped notice is checked against
seen.json. Only ones never seen before move forward — so nothing is sent twice, and nothing is missed even if a scheduled run is skipped. -
3
Classify & summarizesrc/classifier.py
Each new notice is sent to GitHub Models (free, via the workflow's
GITHUB_TOKEN) to assign one of eight categories and write a one-line summary. A keyword fallback keeps things sensible if the model is unavailable. -
4
Notify & persistsrc/notifier.py
Sends one Telegram message per new notice, then commits the updated
seen.jsonand alast_check.txtheartbeat back to the repo — keeping dedup persistent and the schedule alive.
Architecture.
There is no backend to maintain. A GitHub Actions cron job is the entire runtime: it boots a fresh runner four times a day, runs the pipeline, talks to three external services, and commits its own state back to the repository.
State is the clever part. Every run commits seen.json and a
last_check.txt heartbeat back to the repository. The heartbeat resets
GitHub's 60-day inactivity timer — so the scheduled workflow never gets
auto-disabled — while seen.json gives a serverless job the persistent
memory it would otherwise lack.
First-run seeding
On setup the bot silently records the ~20 existing notices with no messages, then only alerts on genuinely new ones.
Flood guard
If a run finds an abnormal burst of "new" notices, it re-seeds instead of flooding your chat — covering site redesigns and long downtime.
Fail-safe state
If scraping returns zero notices, main.py exits with an error and refuses to overwrite state — a layout change can't wipe history.
No run loops
State pushes use the built-in GITHUB_TOKEN, which doesn't trigger new workflow runs — preventing cascading automation.
Single run at a time
A concurrency group ensures only one workflow instance runs at once, so overlapping schedules can't race on state.
Free forever
Public-repo Actions minutes are unlimited and the free GitHub Models tier (150 calls/day) dwarfs the bot's ~20 — no paid keys, DB or server.
Email Trucker
Checks a Gmail inbox roughly every hour via GitHub Actions and sends a Telegram digest — sender + subject — for every new email in the Primary tab. Promotions, Social and Updates are silently skipped, and the mailbox is opened strictly read-only: nothing is ever marked as read. No server, no database. $0 a month.
/ month
hourly, best-effort
marked read
silently skipped
How it works.
No more refreshing Gmail hoping something important slipped in. A single
Telegram chat gets one message per run — and only when there's actually
something new. Every run is a short pipeline, defined in
.github/workflows/check-mail.yml and executed by
check_mail.py.
-
1
Triggercheck-mail.yml
Fires on a cron schedule at
:17past every hour — deliberately off the top-of-the-hour slot, which is the most congested time on GitHub's shared runners — or via a manualworkflow_dispatchrun for testing. -
2
Checkout & secretscheck-mail.yml
Checks out the repo and loads four repository secrets —
GMAIL_ADDRESS,GMAIL_APP_PASSWORD,TELEGRAM_BOT_TOKEN,TELEGRAM_CHAT_ID— as environment variables for the script. -
3
Read mailcheck_mail.py
Logs into Gmail over IMAP with an App Password, mailbox opened read-only, and searches
category:primaryfor messages newer than the last-notified UID instate.json. -
4
Digest & persistcheck_mail.py · state.json
New mail sends one plain-text Telegram digest (sender + subject per email); nothing new just logs and exits. Either way, the last-seen UID (+
UIDVALIDITY) is committed back tostate.jsonso the next stateless run stays duplicate-free. A failed run sends a "⚠️ Email watcher failed" alert through the same bot instead.
Architecture.
Same philosophy as the notice board: no backend to maintain. A GitHub Actions cron job is the runtime — it boots a fresh, disposable runner roughly every hour, reads mail once, and commits its own state back to the repo so the next run — on a completely different machine — knows where it left off.
State is what makes a disposable runner reliable. Every successful run commits the
last-notified UID (plus UIDVALIDITY) to state.json, and the
very next scheduled run — on a brand-new machine — reads it straight back, so
duplicate-free delivery survives GitHub throwing the runner away every single time.
The IMAP session never issues a mark-as-read command, so the inbox itself keeps no
trace of the bot ever visiting it.
First-run baseline
No flood of old mail: the first run silently baselines at the current newest email and sends one "✅ Email watcher is live" confirmation instead.
Stateless-safe dedup
GitHub runners are thrown away after every run — state.json, committed back to the repo, is the only memory the bot has between them.
Best-effort schedule, zero loss
GitHub's cron is best-effort and occasionally skips a slot; a missed hour's mail simply arrives folded into the next digest.
More automation.
Other scripts & pipelines that take repetitive work off my plate. More case studies on the way.