Polls a watchlist of job sources (ATS job boards and curated GitHub repos), detects postings never seen before, filters them, and pushes each new match to Telegram.
Runs on a GitHub Actions cron schedule; every run is stateless and persists its dedup state to seen_jobs.json.
sources.yaml -> FETCH -> NORMALIZE -> DEDUP -> FILTER -> NOTIFY (Telegram)
(adapters) (Job) (seen_jobs.json) (predicates)
Each run fetches everything currently posted, subtracts everything already in seen_jobs.json, and notifies only the remainder.
- Open Telegram and message @BotFather.
- Send
/newbotand follow the prompts (pick a name and a unique username ending inbot). - BotFather replies with an HTTP API token that looks like
123456789:AAE...xyz. This is yourTELEGRAM_BOT_TOKEN. Treat it like a password.
- Send any message (for example
hi) to your new bot so a chat exists. - Open
https://api.telegram.org/bot<YOUR_TOKEN>/getUpdatesin a browser. - Find
"chat":{"id":...}in the JSON response. That number is yourTELEGRAM_CHAT_ID(for a group chat it is negative).
In the repo: Settings -> Secrets and variables -> Actions -> New repository secret.
| Secret | Value |
|---|---|
TELEGRAM_BOT_TOKEN |
the token from BotFather |
TELEGRAM_CHAT_ID |
the chat id from getUpdates |
Secrets are injected into runs as environment variables.
Never put them in sources.yaml or any other tracked file.
Edit sources.yaml.
Adding a company on a supported ATS is one entry, for example:
sources:
- type: ashby
company: wealthsimplepip install -r requirements.txt
python -m scraper.main --dry-run--dry-run prints would-be notifications to stdout instead of sending to Telegram; everything before the notify stage behaves exactly like a real run.
ruff check . # lint
pytest # tests run fully offline (HTTP is mocked)