After running a self-hosted n8n instance on a Hetzner VPS for several months, I decided to simplify. The server was costing me around $30 a month and I was using it mostly for two workflows — a blog automation pipeline and a LinkedIn job alert agent. The blog pipeline I kept, but the job alert agent was a perfect candidate to move to GitHub Actions for free.

This post covers exactly how I built it: what runs where, what each part costs, and the full working code.

The Problem With Manual Job Searches Link to heading

If you are a network security professional looking for senior roles, LinkedIn’s default job alerts are noisy. A search for “Palo Alto” returns everything from junior NOC engineers to product marketing managers. Filtering manually every morning is a waste of time, and most alerts are irrelevant.

What I wanted was simple: run a search daily, score each result against my actual skills and preferences, and email me only the jobs that genuinely match — with an explanation of why.

Architecture Overview Link to heading

The final setup uses three services, all either free or very cheap:

GitHub Actions (free scheduler)
        ↓
Apify — cheap_scraper/linkedin-job-scraper (~$0.06/day)
        ↓
Claude Haiku API — scores each job (~$0.05/day)
        ↓
Gmail SMTP — sends HTML email alert (free)

Total monthly cost: approximately $3–4. No server to manage, no Docker containers, no uptime monitoring.

Why GitHub Actions Instead of a VPS Link to heading

The job alert runs once a day. A VPS running 24 hours to execute a 3-minute task is a poor trade. GitHub Actions gives you 2,000 free minutes per month on the free plan — more than enough for a daily 5-minute workflow.

The tradeoff is that GitHub Actions is not designed for real-time triggers or complex stateful workflows. For a daily batch job like this, it is the right tool.

Why Apify Instead of Firecrawl Link to heading

I initially tried Firecrawl, which is a clean scraping API I use for other things. LinkedIn blocks it outright — the response comes back empty. LinkedIn’s anti-scraping is aggressive and requires residential proxies with specific session handling.

Apify’s cheap_scraper/linkedin-job-scraper actor handles all of that. It uses Playwright with residential proxies under the hood. At $0.40 per 1,000 results, a daily run fetching 150 jobs costs about $0.06. The actor accepts LinkedIn search URLs directly, so I can use the same filtered URLs I would use manually.

The Scoring Logic Link to heading

Rather than keyword matching, I send each job title, company, location and description to Claude Haiku with a detailed scoring prompt. The prompt encodes:

  • My target roles (Network Security Architect, Zero Trust Architect, Senior Network Security Engineer)
  • Core skills to match against (Palo Alto, Zscaler, Netskope, Illumio, Cisco Firepower)
  • Hard exclusions (penetration testing roles, SOC analyst roles, night shift roles, manager titles)
  • Company preference weighting (GCC and product companies score higher, large IT services firms score lower)
  • Location preference (Bangalore and Dubai score highest)

Each job gets a score from 0 to 100. Only jobs scoring 65 or above trigger an email alert.

Claude Haiku is the right model here — it is fast, cheap (around $0.001 per scoring call), and reliable at structured JSON output. Running 50 jobs through it costs about $0.05.

The Workflow File Link to heading

Create .github/workflows/job-alert.yml in your repository:

name: LinkedIn Job Alert - RKS Network Security

on:
  schedule:
    - cron: '30 2 * * *'  # 8:00 AM IST daily
  workflow_dispatch:

jobs:
  job-alert:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pip install requests

      - name: Run job alert script
        timeout-minutes: 30
        env:
          APIFY_API_KEY: ${{ secrets.APIFY_API_KEY }}
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GMAIL_USER: ${{ secrets.GMAIL_USER }}
          GMAIL_APP_PASSWORD: ${{ secrets.GMAIL_APP_PASSWORD }}
          ALERT_EMAIL: your@email.com
        run: python job_alert.py

The cron expression 30 2 * * * runs at 02:30 UTC, which is 08:00 IST. Adjust to match your timezone.

The Python Script Link to heading

The script has four stages: scrape, deduplicate, score, email.

Stage 1 — Scrape via Apify

The Apify actor is triggered via API. The script then polls the run status every 30 seconds until it completes or times out.

APIFY_INPUT = {
    "enrichCompanyData": False,
    "jobType": ["full-time"],
    "locations": ["Bengaluru, Karnataka, India"],
    "publishedAt": "r86400",
    "maxItems": 150,
    "saveOnlyUniqueItems": False,
    "startUrls": [
        {"url": "https://www.linkedin.com/jobs/search/?f_E=4&f_TPR=r86400&geoId=105214831&keywords=%22Network%20Security%20Architect%22%20OR%20%22Security%20Architect%22%20OR%20%22Network%20Security%20Lead%22&refresh=true"},
        {"url": "https://www.linkedin.com/jobs/search/?f_E=4&f_TPR=r86400&geoId=105214831&keywords=%22Palo%20Alto%22%20OR%20%22Zscaler%22%20OR%20%22Netskope%22%20OR%20%22Zero%20Trust%22&refresh=true"}
    ]
}

The publishedAt: r86400 limits results to the past 24 hours so each daily run sees only new postings.

Stage 2 — Deduplicate

The actor sometimes returns the same job from multiple search URLs. A simple title + company key deduplicates before scoring.

Stage 3 — Score with Claude

Each job is sent to Claude Haiku with the scoring prompt. The response is parsed as JSON. Jobs scoring below the threshold are discarded without sending any alert.

Stage 4 — Email

Matched jobs are assembled into an HTML email sorted by score descending. The email shows the score, the match reason Claude generated, the matched skills, and a direct link to the LinkedIn listing.

GitHub Secrets Setup Link to heading

Add these four secrets to your repository under Settings → Secrets and variables → Actions:

SecretValue
APIFY_API_KEYYour Apify API token
ANTHROPIC_API_KEYYour Anthropic API key
GMAIL_USERYour Gmail address
GMAIL_APP_PASSWORD16-character Gmail app password

For the Gmail app password: Google Account → Security → 2-Step Verification → App passwords → Mail.

What the Email Looks Like Link to heading

Each matched job appears as a card showing:

  • Job title and company
  • Score out of 100 with colour coding (green for ≥80, amber for 65–79)
  • Claude’s explanation of why it matched
  • The specific skills that were found in the job description
  • Company type classification (GCC, Product, Startup, MNC Service)
  • A direct link to apply

Lessons From Building This Link to heading

LinkedIn blocks most scrapers. Firecrawl, raw Playwright without proxies, and most other tools return empty results or get rate-limited immediately. Apify’s actor handles this because it uses residential proxies with proper session rotation. Budget for this cost — it is necessary.

Keyword scoring is insufficient. A job titled “Network Security Engineer” at a company that needs someone to manage help-desk tickets is not a match. Sending the full description to an LLM and asking it to reason about fit produces far better signal than regex matching on skill keywords.

Hard exclusions matter as much as positive scoring. The prompt’s exclusion list — penetration testing, SOC analyst, night shift, manager titles — eliminates a large fraction of results before any positive scoring happens. This keeps the email short and relevant.

24-hour filters mean some days have zero results. This is correct behaviour. If nothing relevant was posted today, the script exits cleanly without sending an email. An empty inbox is a good signal.

Cost Summary Link to heading

ComponentMonthly Cost
GitHub ActionsFree
Apify scraping (150 jobs/day)~$1.80
Claude Haiku scoring (50 jobs/day)~$1.50
GmailFree
Total~$3.30

Compared to $30/month for a VPS running this as one of several workflows, the saving is significant if job alerting is the only thing you need.

What I Would Do Differently Link to heading

The script currently fetches 150 jobs from Apify but only scores 50 (limited by the dataset fetch limit). A better approach would be to run a lightweight pre-filter — title matching only — before sending jobs to Claude. This would make the 50 scored jobs more relevant and reduce the chance of wasting scoring budget on obviously irrelevant results.

I may also add a seen-jobs cache using a GitHub repository file or a small Cloudflare KV store, so the same job does not appear in alerts across multiple days if it stays posted for a week.

The full code is available in my GitHub repository at tcprks/rks-job-alert.


Roby KS is a Senior Network Security Engineer with 17 years in IT and 12+ years in network security. He holds PCNSE and Zscaler ZCCA-IA certifications and runs independent security consulting under Rchitect.