Checks ~170 companies' job boards daily and emails you only when a new 2027 SWE intern posting shows up. Runs for free on GitHub Actions - no server, no computer that has to stay on.
Most companies are checked through their ATS's public JSON API (Greenhouse, Lever, Ashby, Workday, SmartRecruiters, plus Amazon's own search JSON and Eightfold boards like Netflix's - real titles, direct application links, ~1s each). Custom career sites fall back to a headless real-Chrome browser, where hits come from actual job links on the page ("link mode") rather than fuzzy text windows - window matching used to turn pagination controls and filter sidebars into "postings" and let a tech posting's keywords justify a non-tech neighbor.
Note: your doc had 21 companies, not 25 - Amazon, Microsoft, Meta,
Apple, Google, Nvidia, Databricks, Stripe, Palantir, Uber, Airbnb,
Salesforce, Adobe, Netflix, Oracle, LinkedIn, Roblox, Coinbase, Spotify,
Snap, TikTok. If there are 4 more you meant to include, just add them to
companies.json the same way (see Step 2).
I checked a few of your companies' pages directly first. None of them run on Greenhouse or Lever (the two ATS platforms with a clean public API) - they're all custom-built career sites, and several (Palantir in particular) only load their actual job listings via JavaScript after the page opens in a browser. A plain HTTP fetch sees an empty shell on those.
So monitor.py now uses a real headless browser (Playwright +
Chromium) to load each page the way a person actually would, then
searches the rendered text. It's still just a script - no LLM calls, no
per-run cost beyond GitHub's free compute minutes.
- Go to github.com, click New repository. Name it something like
intern-watch. Keep it Private. Skip adding a README (you already have one here). - Upload this folder's files via Add file → Upload files on the
repo page (drag-and-drop works, including the
.githubfolder), or push via git if you're comfortable with that.
All 21 companies from your doc are already in there, pointed at the
URLs you gave me, set to "ats": "browser" (the headless-browser mode).
You don't have to do anything here unless you want to add more
companies or swap in a more specific URL - see "If a company never
finds anything" below.
- If using Gmail: turn on 2-Step Verification on your Google account, then create an App Password at myaccount.google.com/apppasswords. Use that (not your normal password) below.
- In your GitHub repo: Settings → Secrets and variables → Actions →
New repository secret. Add these:
SMTP_HOST→smtp.gmail.comSMTP_PORT→587SMTP_USER→ your Gmail addressSMTP_PASS→ the app password from step 1NOTIFY_EMAIL→ where you want alerts sent (can be the same address)
Prefer Slack? Say so and I'll swap the email step for a one-line Slack webhook call.
- Go to the Actions tab → Check Intern Postings → Run workflow (manual trigger).
- Click into the run and watch the log - it prints which company it's checking as it goes, and any errors per-company.
- The first run treats every existing 2027 posting as "new" since there's nothing to compare against yet - expect one slightly noisy email the first time, then only genuinely new postings after that.
It's July 2026 - most of these companies haven't opened Summer 2027 intern applications yet, so a first run turning up zero hits across the board is expected, not necessarily a bug. To sanity-check that the rendering itself is actually working (as opposed to just finding nothing because nothing's posted):
- Temporarily edit
YEAR_PATTERNinmonitor.pyto match2026instead of2027, and run the workflow manually. - If that turns up real 2026 intern postings for at least some
companies, the pipeline itself is working - change it back to
2027and let it run on schedule. - If it turns up nothing even for 2026, that company's page needs a different URL or extra handling (see below).
A few of your URLs are program-overview pages rather than the actual job search/listing page (for example, a "why intern with us" page versus the page with actual role titles). Those will often render fine but just won't contain any job listings to match against. If a specific company keeps coming up empty even after the 2026 sanity check above, look for a "See open roles" / "View all jobs" link on that page and swap in that URL instead - or send it to me and I'll find the right one and adjust the entry.
A couple of others may load results only after you type into a search box or click a filter, which a plain page load won't trigger. If that turns out to be the case for a given company, flag it and I'll add site-specific handling (Playwright can click and type, it just needs a few extra lines per site).
A second, separate watcher: extern_watch.py scrapes extern.com's
per-company internship guides (the "Quick Facts" table each one
carries) for every company with a tech program - Extern's own tech
category plus tech tracks at non-tech companies (banks, retail,
hospitality: think Wells Fargo Technology Summer Analyst) - and
maintains:
InternWatch.xlsx- the human-readable watchlist. Sheet 1 is sorted so whatever Extern expects to open soonest sits at the top (green = window open now, yellow = opening within ~2 months), with each company's career page link, Extern guide link, rolling policy, and pay. Sheet 2 holds companies with no formal intern program. Regenerated every run - don't edit it by hand.extern_calendar.json- the machine-readable state the diff runs against.
It runs daily (.github/workflows/extern-calendar.yml, ~9 AM ET).
You only get an email when something actually changed: a window
moved, a career link changed, a company gained/lost a formal program,
or Extern published brand-new guides (those are listed for review,
never auto-added - some are non-tech). Add or remove companies in
extern_companies.json.
simplify_watch.py polls the community-maintained
SimplifyJobs/Summer2027-Internships
listings hourly (.github/workflows/simplify-watch.yml) and emails
the moment a new posting appears - the community typically reports
postings within hours of them going live. One HTTP request per run,
stdlib only.
Filters (edit TECH_CATEGORIES etc. in the script): Summer 2027,
Software + AI/ML/Data categories, US locations, and postings
requiring U.S. citizenship are dropped (CPT). "No sponsorship"
postings are kept with a note - CPT internships don't need
sponsorship; conversion later would.
underclass_watch.py (runs with the Simplify watch, every 30 min)
watches freshman/sophomore-specific sources - the
underclassmen-opportunities and summer-2027-internships GitHub repos
plus Extern's verified underclassmen program directory (STEP, Explore,
Ignite, Propel, TEIP, ...) - and alerts on new rows. The full dataset
renders as the "Underclassman programs" sheet in InternWatch.xlsx.
Apply to these with the Class of 2029 graduation date.
| Layer | Source | Lag | Job |
|---|---|---|---|
| Calendar | extern.com guides | days-weeks | when to expect each window, tracks, visa stance |
| Live watch | Simplify community list | hours | "a posting just went up" across hundreds of companies |
| Direct checks | company career pages | your cron interval | authoritative check on the shortlist in companies.json |
Schedules run in two tiers. The BASE tier fits a private repo's free 2,000 Actions minutes/month: daily full sweep + fast lane every 2h + Simplify every 2h + daily Extern (~1,850 total). The MAX tier - fast lane every 20 minutes 24/7, Simplify every 30 minutes, three full sweeps a day - is scheduled too but its jobs only run when the repository is PUBLIC (unlimited free minutes); on a private repo GitHub skips them at zero cost. Making the repo public upgrades the cadence automatically, no config change needed.
- Schedule: edit the
cron:line in.github/workflows/check-intern-postings.yml. crontab.guru helps build the expression. - Keyword matching: edit
KEYWORD_PATTERNinmonitor.pyif a company phrases the role unusually. - Add/remove companies: edit
companies.jsonand push.
Free. Loading 21 pages in a headless browser plus a bit of setup time comes to a few minutes per run - even running daily for a full month stays well within GitHub Actions' free 2,000 minutes/month.