Google Sheets + Gmail: job post expiry reminders
Old job links quietly rot. Then a candidate clicks “Apply,” hits a 404, and your team looks sloppy (even when hiring is going fine).
This is the kind of mess that lands on recruiting coordinators first. TA ops folks feel it when they’re cleaning spreadsheets, and hiring managers get dragged in when roles should have been refreshed weeks ago. With job expiry reminders automation, you get a daily nudge system that keeps listings current without anyone babysitting a sheet.
This workflow reads your Google Sheet, checks each job URL for “last updated” signals, and emails the right recruiter when a post is stale. You’ll see exactly what it does, what you need, and how to tailor it to your process.
How This Automation Works
The full n8n workflow, from trigger to final output:
n8n Workflow Template: Google Sheets + Gmail: job post expiry reminders
flowchart LR
subgraph sg0["Schedule Flow"]
direction LR
n0@{ icon: "mdi:play-circle", form: "rounded", label: "Schedule Trigger", pos: "b", h: 48 }
n1@{ icon: "mdi:database", form: "rounded", label: "Fetch Job URLs", pos: "b", h: 48 }
n2@{ icon: "mdi:swap-horizontal", form: "rounded", label: "Filter Invalid Rows", pos: "b", h: 48 }
n3["<div style='background:#f5f5f5;padding:10px;border-radius:8px;display:inline-block;border:1px solid #e0e0e0'><img src='https://flowpast.com/wp-content/uploads/n8n-workflow-icons/httprequest.dark.svg' width='40' height='40' /></div><br/>HTTP Request"]
n4@{ icon: "mdi:cog", form: "rounded", label: "Wait Before HTTP", pos: "b", h: 48 }
n5@{ icon: "mdi:swap-horizontal", form: "rounded", label: "Is Age ≥ THRESHOLD_DAYS?", pos: "b", h: 48 }
n6@{ icon: "mdi:message-outline", form: "rounded", label: "Gmail", pos: "b", h: 48 }
n7["<div style='background:#f5f5f5;padding:10px;border-radius:8px;display:inline-block;border:1px solid #e0e0e0'><img src='https://flowpast.com/wp-content/uploads/n8n-workflow-icons/code.svg' width='40' height='40' /></div><br/>calculate Age"]
n8@{ icon: "mdi:swap-vertical", form: "rounded", label: "Log Invalid Rows", pos: "b", h: 48 }
n9["<div style='background:#f5f5f5;padding:10px;border-radius:8px;display:inline-block;border:1px solid #e0e0e0'><img src='https://flowpast.com/wp-content/uploads/n8n-workflow-icons/code.svg' width='40' height='40' /></div><br/>Check Weekend"]
n10@{ icon: "mdi:swap-horizontal", form: "rounded", label: "Weekend Filter", pos: "b", h: 48 }
n11["<div style='background:#f5f5f5;padding:10px;border-radius:8px;display:inline-block;border:1px solid #e0e0e0'><img src='https://flowpast.com/wp-content/uploads/n8n-workflow-icons/code.svg' width='40' height='40' /></div><br/>Log weekend skip"]
n12@{ icon: "mdi:swap-vertical", form: "rounded", label: "configuration sheet", pos: "b", h: 48 }
n13@{ icon: "mdi:swap-vertical", form: "rounded", label: "configure mapping", pos: "b", h: 48 }
n3 --> n13
n9 --> n10
n7 --> n5
n1 --> n2
n10 --> n4
n10 --> n11
n0 --> n12
n4 --> n3
n13 --> n7
n2 --> n9
n2 --> n8
n12 --> n1
n5 --> n6
end
%% Styling
classDef trigger fill:#e8f5e9,stroke:#388e3c,stroke-width:2px
classDef ai fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
classDef aiModel fill:#e8eaf6,stroke:#3f51b5,stroke-width:2px
classDef decision fill:#fff8e1,stroke:#f9a825,stroke-width:2px
classDef database fill:#fce4ec,stroke:#c2185b,stroke-width:2px
classDef api fill:#fff3e0,stroke:#e65100,stroke-width:2px
classDef code fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
classDef disabled stroke-dasharray: 5 5,opacity: 0.5
class n0 trigger
class n2,n5,n10 decision
class n1 database
class n3 api
class n7,n9,n11 code
classDef customIcon fill:none,stroke:none
class n3,n7,n9,n11 customIcon
The Problem: Stale Job Posts Create Silent Damage
Keeping job posts “fresh” sounds simple until you’re managing dozens of links across boards, partner pages, and your own career site. Someone updates the role description in one place, forgets the other, and now your spreadsheet is a graveyard of outdated URLs. The worst part is you don’t notice immediately. You notice after a candidate flags it, after a recruiter wastes time reopening a closed requisition, or after you’ve already paid for a week of sponsored visibility to a role that should have been paused.
The friction compounds. Not because any single link is catastrophic, but because the backlog never stops growing.
- Each week, someone ends up spot-checking job URLs by hand, which can easily eat about 1-2 hours.
- Recruiters don’t own every posting location, so follow-ups turn into a messy game of “who touched this last?”
- Outdated posts lead to the wrong candidate expectations and awkward conversations in early screens.
- Because there’s no consistent “expiry” signal, reminders are reactive instead of scheduled and reliable.
The Solution: Daily URL Checks + Polite Gmail Nudges
This n8n workflow turns your Google Sheet into a lightweight job post monitoring system. Every day at 10:00 AM Asia/Kolkata, it reads the “Job Posts” tab (job_url, recruiter_name, recruiter_email), validates that each row is usable, and then checks the job page itself. It starts with a fast HTTP HEAD request to look for a Last-Modified header. If the site doesn’t provide that header, it follows up with a GET request and scans common “last updated” meta tags like article:modified_time or dateModified. Once it has a best-available “last changed” date, it calculates the age in days in your chosen timezone and compares it to your threshold (default 30 days). If the listing looks stale, it sends a polite, personalized Gmail reminder to the recruiter who owns that row.
The workflow starts on a schedule, then pulls job links from Google Sheets and checks each URL with conservative rate limits. After the age calculation and a simple threshold check, Gmail sends the nudge (or produces a preview if DRY_RUN is enabled).
What You Get: Automation vs. Results
| What This Workflow Automates | Results You’ll Get |
|---|---|
|
|
Example: What This Looks Like
Say you’re tracking 40 live job URLs in a Google Sheet. Manually checking them is maybe 2 minutes per link once you account for page loads and quick scanning, which is about 80 minutes every week (and it usually gets postponed). With this workflow, the daily run is scheduled at 10:00, does the HTTP checks in the background with built-in delays, and sends only the relevant Gmail nudges. Your “work” becomes reading a few emails and refreshing or unlisting the roles that crossed the 30-day threshold.
What You’ll Need
- n8n instance (try n8n Cloud free)
- Self-hosting option if you prefer (Hostinger works well)
- Google Sheets to store job URLs and owners.
- Gmail (SMTP) to send the reminder emails.
- Google App Password (create it in your Google Account security settings if 2FA is on).
Skill level: Beginner. You’ll connect Google Sheets + Gmail and edit a few config fields like timezone and the stale-day threshold.
Don’t want to set this up yourself? Talk to an automation expert (free 15-minute consultation).
How It Works
A scheduled run kicks things off. The workflow triggers every day at 10:00 AM Asia/Kolkata, so the hygiene check happens even when the team is busy or out.
Your Sheet becomes the job queue. n8n pulls rows from the Google Sheet tab named “Job Posts” and checks that each row has a valid URL and recruiter email. Bad rows get skipped cleanly so the rest still process.
The workflow checks “last updated” without scraping everything. It uses an HTTP HEAD request first (fast, lighter on the website). If the page doesn’t provide Last-Modified, it falls back to a GET request and looks for common update timestamps in meta tags, then computes age in days.
Gmail sends the nudge only when it matters. If age_days is at or above your THRESHOLD_DAYS (30 by default), it builds a personalized email from your templates and sends it, spacing emails out with rate limits.
You can easily modify the threshold and weekend rules to match how your team works. See the full implementation guide below for customization options.
Step-by-Step Implementation Guide
Step 1: Configure the Schedule Trigger
Set the automation schedule so the workflow runs at the desired time.
- Add and open Scheduled Run Initiator.
- Set the schedule rule to the cron expression
10 32 10 * * *. - Confirm Scheduled Run Initiator connects directly to Config Values Set.
Step 2: Configure Workflow Defaults and Constants
Define all constants used across the workflow (spreadsheet ID, thresholds, timeouts, and templates).
- Open Config Values Set and set SPREADSHEET_ID to
[YOUR_ID]. - Set SOURCE_SHEET to
Job Postsand TIMEZONE toAsia/Kolkata. - Set THRESHOLD_DAYS to
30, USER_AGENT ton8n-job-checker/1.0, and HTTP_TIMEOUT_SECONDS to10. - Set rate limiting values: RATE_LIMIT_HTTP_SECONDS to
5and RATE_LIMIT_EMAIL_SECONDS to2. - Set messaging templates: SUBJECT_TEMPLATE to
Please review your stale job post, HTML_TEMPLATE to<p>Hello {{recruiter_name}},</p><p>Your job post at {{job_url}} is {{age_days}} days old (last updated on {{last_modified}}). Consider updating it.</p>, and TEXT_TEMPLATE toHi {{recruiter_name}}, your job {{job_url}} is {{age_days}} days old. Last updated on {{last_modified}}.. - Set INCLUDE_WEEKENDS to
true(orfalseif you want to skip weekend runs) and DRY_RUN tofalse.
⚠️ Common Pitfall: Leaving [YOUR_ID] or [YOUR_EMAIL] in Config Values Set will cause the workflow to read the wrong sheet or send from an invalid address.
Step 3: Connect Google Sheets and Pull Job Links
Retrieve all job post rows from the Google Sheet.
- Open Retrieve Job Links and set Document ID to
[YOUR_ID]. - Set Sheet Name to
Job Posts. - Credential Required: Connect your
googleSheetsOAuth2Apicredentials. - Ensure Config Values Set connects to Retrieve Job Links, and Retrieve Job Links connects to Validate Row Data.
Step 4: Validate Rows and Enforce Weekend Rules
Filter invalid rows, track skipped rows, and optionally block runs on weekends.
- In Validate Row Data, confirm the conditions use
{{ $json.job_url }},{{ $json.recruiter_email }}, and the email regex^[^\s@]+@[^\s@]+\.[^\s@]+$. - Connect the true path of Validate Row Data to Weekend Eligibility Check and the false path to Record Skipped Rows.
- In Record Skipped Rows, keep the reason expression
{{ $json.job_url ? ($json.recruiter_email ? ($json.recruiter_name ? 'Unknown validation error' : 'Missing recruiter name') : 'Invalid email format') : 'Missing job URL' }}and set status toSKIPPED. - In Weekend Eligibility Check, keep the JavaScript that reads
$('Config Values Set').item.json.INCLUDE_WEEKENDSand setsstatustoPROCESSINGorSKIPPED_WEEKEND. - In Process Weekend Gate, confirm the condition is
{{ $json.status }}equalsPROCESSING; route the false branch to Note Weekend Skip.
false, the workflow will route to Note Weekend Skip and avoid HTTP calls on weekends.Step 5: Probe Job URLs and Compute Age
Request headers, extract Last-Modified, and compute age in days.
- Ensure Process Weekend Gate connects to Delay Before Head Call and then to Head Request Probe.
- In Head Request Probe, set URL to
{{$json.job_url}}, Method toHEAD, and enable Send Headers. - Set the User-Agent header to
{{ ($json["USER_AGENT"] || "n8n-job-checker/1.0").replace(/\n/g, "").trim() }}and keep the timeout at5000ms. - In Map Header Fields, set last_modified to
{{ $json.headers && $json.headers['last-modified'] ? $json.headers['last-modified'] : 'Unknown' }}. - In Map Header Fields, set recruiter_email to
=={{ $json.recruiter_email }}, recruiter_name to{{ $json.recruiter_name }}, and job_url to{{ $json.job_url }}. - In Compute Post Age, keep the JavaScript that calculates
age_daysfromlast_modifiedand outputs the computed value. - Confirm Compute Post Age flows to Threshold Age Check.
⚠️ Common Pitfall: The Map Header Fields assignment for recruiter_email includes a double equals (==). Keep it exactly as in the workflow if you are recreating it; changing it may alter the data mapping.
Step 6: Configure Email Alerts for Stale Posts
Send an email when a job post exceeds the age threshold.
- In Threshold Age Check, verify the condition is
{{ $json.age_days }}gte{{ 30 }}. - Connect the true branch of Threshold Age Check to Send Email Alert.
- In Send Email Alert, set Send To to
{{ $json.recruiter_email }}. - Set the Subject to
{{'Gentle Reminder: Update Your Job Post (${json.age_days} days old)' }}. - Set the Message body to the HTML template starting with
<p>Hi {{ $json.recruiter_name }},</p>and including{{ $json.age_days }},{{ $json.job_url }}, and{{ $json.last_modified }}. - Credential Required: Connect your
gmailOAuth2credentials.
Step 7: Test & Activate Your Workflow
Run a controlled test to verify data flow, then activate the schedule.
- Click Execute Workflow to run a manual test from Scheduled Run Initiator.
- Confirm rows with valid data pass Validate Row Data, then flow through Weekend Eligibility Check → Process Weekend Gate → Delay Before Head Call.
- Verify Head Request Probe returns headers and Map Header Fields sets last_modified correctly before Compute Post Age.
- Check that Threshold Age Check only sends emails when
age_daysis at or above30. - Toggle the workflow Active to enable scheduled runs in production.
Common Gotchas
- Google Sheets credentials can expire or need the right access to the spreadsheet. If things break, check the n8n Credentials panel and confirm the account can open the Sheet ID.
- If you’re using Wait nodes or external sites respond slowly, processing times vary. Bump up HTTP_TIMEOUT_SECONDS or RATE_LIMIT_HTTP_SECONDS if downstream nodes fail or pages return empty headers.
- Gmail sending can fail due to SMTP auth rules or daily quotas. Start by verifying you’re using an app password (when 2FA is enabled) and that SMTP_FROM matches the account you authenticated.
Frequently Asked Questions
About 30 minutes if your Sheet and Gmail account are ready.
No coding required. You’ll mostly connect accounts and update the Config fields like THRESHOLD_DAYS and TIMEZONE.
Yes. n8n has a free self-hosted option and a free trial on n8n Cloud. Cloud plans start at $20/month for higher volume. You’ll also need to factor in your email provider costs (Gmail is usually included) and any hosting costs if you self-host.
Two options: n8n Cloud (managed, easiest setup) or self-hosting on a VPS. For self-hosting, Hostinger VPS is affordable and handles n8n well. Self-hosting gives you unlimited executions but requires basic server management.
Yes, and it’s a simple tweak. Change the Scheduled Run Initiator (the cron trigger) to weekly, then keep the rest of the logic the same. Common customizations also include setting THRESHOLD_DAYS to 15 or 45, turning INCLUDE_WEEKENDS on or off, and editing SUBJECT_TEMPLATE/HTML_TEMPLATE/TEXT_TEMPLATE to match your tone.
Most of the time it’s SMTP authentication. If your Google account uses 2FA, you need an app password and you must update it in the Gmail/SMTP credentials in n8n. Also check that your SMTP_FROM matches the sending mailbox, and watch for quota limits if you’re emailing a lot of recruiters at once.
Practically, hundreds of rows is fine as long as you keep the built-in rate limits. On n8n Cloud, your monthly execution limit depends on your plan; on self-hosted n8n there’s no platform limit, it just depends on your server and how aggressively websites throttle requests.
Often, yes, because this is not a simple “Sheet row → send email” zap. You’re doing conditional validation, HTTP header checks, a fallback GET parse, timezone-based date math, and rate limiting, and n8n handles that kind of branching cleanly without turning into a spaghetti bill. Zapier or Make can still work if you simplify the logic (for example, you store last_checked_at and age_days yourself and just trigger reminders), but you’ll end up doing more manual maintenance. If you want help choosing the right approach for your volume and team, Talk to an automation expert.
Once this is running, job link hygiene stops being a “someone should really…” task. The workflow handles the routine checks, and your team only spends time on the roles that actually need attention.
Need Help Setting This Up?
Our automation experts can build and customize this workflow for your specific needs. Free 15-minute consultation—no commitment required.