DEV Community

Olly
Olly

Posted on

Track job applications from Gmail automatically with n8n and AI

After the twentieth job application, nobody remembers which company replied, which one sent a coding test with a Friday deadline, and which one quietly rejected you three weeks ago. The usual advice is "keep a spreadsheet". The problem is that keeping the spreadsheet is a second job.

But almost every update in a job search already arrives as an email: the "thanks for applying" confirmation, the assessment link, the interview invite, the rejection. So the spreadsheet can maintain itself. This post walks through an n8n workflow that watches Gmail, uses Google Gemini (free tier) to recognise emails about your applications, and keeps one row per application in Google Sheets with the latest status, next step and deadline.

What you end up with

A Google Sheet like this, updated every hour:

application_key | company | role | status | next_step | deadline | last_update | last_subject | from | gmail_link
Enter fullscreen mode Exit fullscreen mode

Statuses: applied, assessment, interview, offer, rejected, recruiter_outreach, other. Add conditional formatting on the status column and you have a colour-coded board.

The architecture

Gmail Trigger (every hour, filtered search)
  -> Code: keep sender, subject, date, first 3,000 chars
  -> Basic LLM Chain + Gemini + Structured Output Parser
  -> IF is_job_application
  -> Set: build application_key + Gmail link
  -> Google Sheets: append or update by application_key
Enter fullscreen mode Exit fullscreen mode

Six working nodes. The interesting decisions are in the filter, the prompt and the key.

Step 1: Pre-filter in Gmail, not in the AI

You don't want to send every newsletter and receipt to an LLM. The Gmail Trigger node accepts a normal Gmail search query, so let Gmail do the first pass:

-category:promotions -category:social (application OR applying OR applied OR interview OR candidacy OR position OR role OR offer OR assessment)
Enter fullscreen mode Exit fullscreen mode

This is deliberately broad: a missed interview invite is far worse than a wasted AI call. The model does the precise filtering in the next step.

Set the trigger to Simple: off so you get the full message (body text and HTML), and Read status: both, because you may already have opened the email on your phone before the workflow sees it.

Step 2: Trim the email

Rejection emails are short, but some ATS emails come with 40 KB of HTML. A Code node keeps only what the model needs:

return $input.all().map(i => {
  const m = i.json;
  const body = (m.text || (m.html || '').replace(/<[^>]+>/g, ' ') || m.snippet || '')
    .replace(/\s+/g, ' ').trim();
  return { json: {
    message_id: m.id, thread_id: m.threadId,
    from: m.from?.text || m.from?.value?.[0]?.address || '',
    subject: m.subject || '', date: m.date || '',
    body: body.slice(0, 3000)
  } };
});
Enter fullscreen mode Exit fullscreen mode

3,000 characters is enough for every status email I've seen, and it keeps each call small on the free tier.

Step 3: Ask the right question

The prompt does two jobs at once: decide whether this email is about your application at all, and if so extract the details.

You read one email from a job seeker's inbox and decide whether it is about one of
their own job applications.

From: {{ $json.from }}
Subject: {{ $json.subject }}
Date: {{ $json.date }}
Body:
{{ $json.body }}

Rules:
- is_job_application is true only for emails about an application this person made or a
  recruiter contacting them about a specific role. Newsletters, job alerts, marketing and
  course emails are false.
- status is one of: applied (confirmation received), assessment (test or task),
  interview (invite or scheduling), offer, rejected, recruiter_outreach, other.
- company and role: use the hiring company (not the ATS such as Workday or Greenhouse).
  Use "Unknown" if not stated.
- deadline: an ISO date (YYYY-MM-DD) if the email names one, otherwise an empty string.
- next_step: under 12 words, empty string if none.
Enter fullscreen mode Exit fullscreen mode

The line about the ATS matters more than you'd think. Lots of confirmation emails come from no-reply@myworkday.com or greenhouse.io, and without that instruction you end up with a tracker full of "Workday".

Job alerts are the other big false positive: "10 new Data Analyst jobs in London" contains every keyword in the Gmail filter. The rule explicitly marks them false.

A Structured Output Parser pins the shape:

{
  "is_job_application": true,
  "company": "Acme Ltd",
  "role": "Data Analyst",
  "status": "interview",
  "next_step": "Video interview on Tuesday",
  "deadline": "2026-10-21",
  "confidence": "high"
}
Enter fullscreen mode Exit fullscreen mode

In the LLM Chain node, use batching (5 items, 5-second delay). When you first activate and a burst of emails arrives, this keeps you under the free tier's requests-per-minute limit.

Step 4: One row per application, not per email

This is the part that turns a log into a tracker. Each email produces a key from company and role:

{{ ($json.output.company + '|' + $json.output.role).toLowerCase().replace(/[^a-z0-9|]+/g, ' ').trim() }}
Enter fullscreen mode Exit fullscreen mode

Acme Ltd | Data Analyst and ACME LTD | Data Analyst. both become acme ltd|data analyst. The Google Sheets node uses Append or Update with application_key as the matching column, so the confirmation creates the row, and the assessment, the interview invite and the offer all update the same row.

The row also stores a direct Gmail link to the thread:

https://mail.google.com/mail/u/0/#all/{{ thread_id }}
Enter fullscreen mode Exit fullscreen mode

so you can jump from the sheet to the email in one click.

Setup

  1. Import the workflow. Add a Gmail OAuth2 credential on the trigger.
  2. Add a free Google Gemini (PaLM) API credential (key from aistudio.google.com) on the chat model.
  3. Create a Google Sheet with an Applications tab and the header row above. Put its ID in the Sheets node and connect Google Sheets OAuth2.
  4. Set your timezone in Workflow settings.
  5. Activate.

Gotchas

  • The trigger only sees new mail. Gmail Trigger picks up emails that arrive after activation. To backfill existing applications, run a one-off copy with a Gmail "Get many messages" node and the same search, then pipe it into the same chain.
  • Company names drift. "Acme" in the confirmation and "Acme Group" in the interview invite will create two rows. It doesn't happen often, but when it does, fix the company cell by hand and future emails will match whichever spelling the AI picks next. A small alias tab is an easy upgrade.
  • "Unknown" roles. Some rejections don't mention the role at all. Those land on an acme|unknown row. Glance at those occasionally and merge by hand.
  • Privacy. Email content goes to the model provider. If that's a concern, swap the Gemini node for a local Ollama model; the prompt and parser are unchanged.

Going further

The free version gives you a self-updating tracker. When I used it myself, the things I wanted next were: an instant ping when an interview or offer lands (rather than finding it in the sheet), a weekly view of how the search is going, and a nudge for applications that went quiet. Those are in a Pro version: a History tab with every status change, instant Telegram alerts for interviews, assessments and offers, a Monday report (applications, response rate, interviews, offers, rejections), and polite follow-up emails written by Gemini for applications with no reply after N days, saved as Gmail drafts (never sent automatically). It's $15 on Payhip: Job application tracker Pro.

If you're earlier in the search, there's also a free template on n8n.io that scores remote job listings against your CV and alerts you on Telegram. The rest of my templates are on the store and my n8n creator profile.

Good luck with the search. If an email type confuses the classifier, paste the subject line in the comments and I'll tune the prompt.

Top comments (0)