An n8n agent that finds and ranks internships automatically — so I don't have to search every week.
Intern8n automates internship hunting for a final-year student workflow. It searches Google Jobs via SerpApi, filters for actual internships, deduplicates against a Google Sheet of seen listings, ranks the remainder with an LLM against the requested role, and emails the top 5 matches. Two triggers feed the same pipeline: an on-demand chat trigger and a weekly schedule trigger that makes it a true agent — it runs even without manual input.
I was already searching for internships manually every week. The assignment was to build an agent for a task I already do by hand, so I picked this one. Keeping it fully free-tier (SerpApi, Groq, Gemini, Google Sheets, Gmail via n8n) was a constraint from the start.
Workflow in the n8n editor: two triggers converge on the same pipeline (screenshot: intern-n8n.png).
┌──────────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐ ┌────────────┐
│ Schedule Trigger │──▶│ Edit Fields │──▶│ Google Jobs │──▶│ code1 │──▶│ Google │
│ (weekly, Tue) │ │ (role input) │ │ (SerpApi) │ │ (filter) │ │ Sheets │
└──────────────────┘ └──────────────┘ └─────────────┘ └─────────────┘ │ (read) │
▲ └──────────────┘
│ │
┌──────────────────┐ ▼
│ When chat message│ ┌──────────────┐
│ received │──────────────────────────────────────────────────▶│ dedup-filter │
└──────────────────┘ │ (vs. Sheet) │
└──────────────┘
│
▼
┌────────┐
│ If │── false ──▶ Code in JavaScript ──▶ Email: No New Matches
│jobs>0? │ (no new listings)
└────────┘
│ true
▼
┌────────────────┐
│ Basic LLM Chain│── Success ──▶ code2 ──┬──▶ Email: Weekly Digest
│ Groq → Gemini │ (rank/ │ (up to 5)
│ (fallback) │ format) └──▶ Split Out ──▶ Mark as seen (Sheet)
└────────────────┘
│ Error
▼
LLM Failure Handler ──▶ Email: Ranking Failed (raw listings)
Flow:
Schedule Trigger(weekly) orWhen chat message receivedprovideschatInput(e.g., "AI/ML Engineer")Google_jobs search(SerpApi) fetches Google Jobs listingscode1filters for internships only; generates stablejob_idand keepsapply_options/share_linkGet row(s) in sheet+dedup-filterremoves jobs already in the SheetIfbranches: no new jobs → email "No new matches"; has jobs →Basic LLM ChainBasic LLM Chain(Groqopenai/gpt-oss-120b→ fallback Gemini) scores/ranks top 5; on error →LLM Failure Handleremails raw listingscode2builds markdown,Email — Weekly Internship Digestsends HTML,Split Out+Mark as seenappendsjob_ids to Sheet
- Two triggers, one pipeline — on-demand chat search + autonomous weekly agent run (Tuesdays, weekly interval)
- Internship-only filtering — code node drops full-time/non-intern listings before LLM sees them
- Deduplication — Google Sheets
job_idstore; never emails the same listing twice - LLM ranking with guardrails — prompt instructs "copy
job_idexactly", top 5 only, score 1-10; application links are pulled from SerpApi data in code, LLM never touches URLs (fixes earlier hallucinated-URL bug) - Failure handling —
Basic LLM Chainset tocontinueErrorOutput; error branch builds raw HTML viaLLM Failure Handlerand emails "ranking failed" so runs never fail silently; tested by deliberately disconnecting both LLM providers - Free-tier only — SerpApi free tier, Groq + Gemini free tiers, n8n self-hosted
| Layer | Technology |
|---|---|
| Orchestration | n8n (workflow flyrank-internship-agent-v2.json) |
| Jobs API | SerpApi — google_jobs engine |
| Filtering / Dedup | n8n Code nodes (JavaScript), Google Sheets |
| Ranking | Basic LLM Chain + Groq (openai/gpt-oss-120b) + Google Gemini (fallback) |
| Notifications | Gmail (3 templates: Digest / No Matches / Ranking Failed) |
| State | Google Sheets (job_id column, appendOrUpdate) |
- n8n instance (self-hosted or n8n Cloud)
- SerpApi API key
- Groq API key and/or Google Gemini API key
- Google Sheets OAuth + a Sheet with a
job_idcolumn - Gmail OAuth
# 1. Clone (after you recreate the repo)
git clone https://github.com/NameRectified/intern8n.git
cd intern8n
# 2. In n8n: Workflows → Import from File → select flyrank-internship-agent-v2.json- SerpApi — Credentials → SerpApi → paste key → assign to
Google_jobs search - Groq → assign to
Groq Chat Model - Google Gemini (PaLM) → assign to
Google Gemini Chat Model - Google Sheets OAuth2 → assign to
Get row(s) in sheet+Mark as seen; setdocumentIdto your Sheet ID (replaceYOUR_GOOGLE_SHEET_IDplaceholder) - Gmail OAuth2 → assign to all 3 Email nodes; replace
YOUR_EMAIL@gmail.com
- Schedule Trigger: default is
every week on Tuesday; adjust in the node - Edit Fields: default
chatInput = "AI/ML Engineer"— used by the schedule path - Chat Trigger: send any role string (e.g.,
ai engineer) to run on demand
- Manual: click
Execute workflowor send a chat message - Scheduled: activate the workflow; it emails automatically each week
intern8n/
├── flyrank-internship-agent-v2.json # n8n workflow export (sanitized placeholders)
├── intern-n8n.png # Workflow screenshot (editor)
├── README.md
└── .gitignore
Weekly Digest email (success, up to 5):
<h2>Top Internship Matches</h2>
<h3>1. AI/ML Engineer Intern</h3>
<strong>Company:</strong> Example Labs<br>
<strong>Location:</strong> Bengaluru, India<br>
<strong>Posted:</strong> 2 days ago<br>
<strong>Match:</strong> 9/10<br>
<strong>Why:</strong> Direct match for AI/ML Engineer, internship for students...<br>
<strong>Apply:</strong> <a href="...">Apply on LinkedIn</a> <a href="...">Company site</a>No new listings:
No new internship matches found this week.
Ranking failed (LLM rate-limited / down):
<h2>Internship Ranking Failed</h2>
<p>The AI ranking step failed this week, but new listings were found.</p>
<hr><h3>1. Backend Intern</h3> ... <a href="...">Apply</a>All ranking-failure, dedup-miss, and success paths still email — no silent failures.
Why SerpApi Google Jobs? Generous free tier; direct apply_options + share_link in the response. Alternative job APIs were paid or lacked India coverage.
Why pull apply links in code instead of from the LLM? Early version let the LLM return URLs — it hallucinated real jobs with wrong links. Fix: keep application_links from SerpApi in code1, forbid LLM from emitting URLs in the prompt ("Do not include any application URLs", "Return only job_id, score, reason"), re-attach links in code2 by job_id. Verified code1/code2 always use SerpApi links verbatim.
Why Groq + Gemini fallback + failure handler? Sticking to free tiers means rate limits. Basic LLM Chain primary = Groq, fallback = Gemini, onError: continueErrorOutput routes to LLM Failure Handler. Deliberately disconnecting both providers emails the raw listings instead of failing silently.
Why Google Sheets for dedup? Free, visible, and n8n has a native node. job_id is a stable base64 of title|company|location (24 chars); dedup-filter set-diffs against all rows. Mark as seen uses appendOrUpdate on job_id.
Why two triggers? Chat trigger covers on-demand searches; Schedule Trigger (weekly) is what makes it an agent per the brief — autonomous execution without input, emailing results.
- Deploy for real scheduling (free host for n8n — e.g., Render/Fly/Hugging Face Space with cron) and validate weekly runs in production
- Prove failure handler under real rate-limit conditions (not just deliberate disconnect test)
- Add role/location parameterization via Sheet or chat config instead of hard-coded
Edit Fields - Add tests for
code1/dedup-filter/code2logic outside n8n - Add Sheets cleanup/retention for old
job_ids
MIT
