Auto-respond to job applications with next steps
Acknowledge applicants on receipt, log them to a hiring sheet, and email the next step in the process.
Published Nov 11, 2025
When a job advert goes live, the applications arrive in bursts — and a candidate
who hears nothing for a week assumes the worst and moves on. Northwind’s hiring
inbox, [email protected], was getting acknowledged in batches whenever
someone remembered, which meant good applicants slipping away and the same
“did we ever reply to this one?” question coming up every Monday.
This script closes that gap. It watches a Gmail label for new applications,
sends each applicant a same-day acknowledgement that sets a clear expectation,
and logs them to an Applicants sheet so the hiring pipeline is always current.
Every thread it handles gets a second label, so nobody is ever emailed twice.
What you’ll need
- An
Applicantssheet with a header row carrying these columns, in this order:receivedAt,name,email,role,stage. - A Gmail filter that applies the label
careers/newto inbound application emails — for example, anything sent to[email protected]. - Nothing else to set up: the script creates the
careers/acknowledgedlabel itself on first run.
The script
// The Applicants sheet that records every candidate.
const APPLICANTS_SHEET_ID = '1abcApplicantsSheetId';
// Label your Gmail filter applies to new applications.
const NEW_LABEL = 'careers/new';
// Label this script adds once a thread has been handled.
const DONE_LABEL = 'careers/acknowledged';
// The same-day acknowledgement sent to every applicant.
const REPLY =
'Thanks for applying to Northwind. We review every application' +
' and reply within 5 working days with a yes or a no. — Awadesh';
/**
* Finds unhandled application threads, replies to each applicant with
* the next-steps message, logs them to the Applicants sheet, and labels
* the thread so it is never processed again.
*/
function processApplications() {
// 1. Get the inbound label, and the "done" label (creating it if needed).
const incoming = GmailApp.getUserLabelByName(NEW_LABEL);
if (!incoming) {
Logger.log('Label "' + NEW_LABEL + '" does not exist — nothing to do.');
return;
}
const seen =
GmailApp.getUserLabelByName(DONE_LABEL) ||
GmailApp.createLabel(DONE_LABEL);
// 2. Keep only threads that have not already been acknowledged.
const threads = incoming.getThreads().filter(
(t) => !t.getLabels().some((l) => l.getName() === DONE_LABEL)
);
if (!threads.length) {
Logger.log('No new applications to process.');
return;
}
const sheet = SpreadsheetApp.openById(APPLICANTS_SHEET_ID).getSheets()[0];
// 3. Handle each application thread in turn.
for (const thread of threads) {
const msg = thread.getMessages()[0];
const from = msg.getFrom();
// Split "Jane Doe <[email protected]>" into a name and an address.
const email = (from.match(/<(.+?)>/) || [, from])[1];
const name = from.replace(/<.+?>/, '').replace(/"/g, '').trim();
// 4. Reply to the applicant and mark the thread as handled.
thread.reply(REPLY);
thread.addLabel(seen);
// 5. Log the applicant to the hiring sheet at the "received" stage.
sheet.appendRow([
new Date(),
name,
email,
parseRole(msg.getSubject()),
'received',
]);
}
Logger.log('Processed ' + threads.length + ' application(s).');
}
/**
* Pulls a role name out of the email subject. Expects a subject like
* "Application: Senior Designer"; falls back to "unspecified".
*/
function parseRole(subject) {
const m = subject.match(/application[:\s]+(.+)/i);
return m ? m[1].trim() : 'unspecified';
}
How it works
processApplicationslooks up thecareers/newlabel your filter applies, and thecareers/acknowledgedlabel — creating the second one if it does not yet exist.- It filters the inbound threads down to those that are not already carrying
the
careers/acknowledgedlabel, so a thread is only ever handled once. If nothing is left, it logs a message and stops. - For each thread it reads the first message and splits the
Fromheader into a display name and an email address with a small regular expression. - It sends the
REPLYacknowledgement back on the same thread and adds thecareers/acknowledgedlabel immediately, so a mid-run failure cannot cause a double email. - It appends a row to the
Applicantssheet with a timestamp, the candidate’s name and email, the role pulled from the subject byparseRole, and a starting stage ofreceived.
Example run
An applicant emails [email protected]:
From: Jane Doe <[email protected]> Subject: Application: Senior Designer
After the next run, Jane has a reply in her inbox with the 5-working-day
promise, and the Applicants sheet has a new row:
| receivedAt | name | role | stage | |
|---|---|---|---|---|
| 2026-05-25 09:32 | Jane Doe | [email protected] | Senior Designer | received |
The thread now carries careers/acknowledged, so the next run skips it.
Trigger it
Run this on a time-driven trigger so applicants hear back quickly:
- In the Apps Script editor, open Triggers (the clock icon).
- Click Add Trigger.
- Choose
processApplications, event source Time-driven, type Minutes timer, and Every 30 minutes. - Save and approve the authorisation prompt.
A 30-minute cadence keeps every reply same-day without polling Gmail constantly.
Watch out for
- The script replies to the thread’s first message, so a candidate emailing a reply onto an existing acknowledged thread will not be processed again — that is by design, but means follow-up emails need handling by a person.
parseRoledepends on the subject containing the word “application”. If your job board sends a different subject line, applicants will be logged asunspecified— adjust the regular expression to match your advert format.thread.replysends from the account running the script. Ifcareers@is a shared mailbox or group, run the script from an account with send-as rights, or the reply will appear to come from the wrong address.- Gmail enforces a daily send quota (around 100 emails on a consumer account, more on Workspace). A large advert that draws hundreds of applications in a day can hit that ceiling — the unsent threads will simply be picked up on the next run once the quota resets.
- The sheet grows forever. Archive or filter old rows once a role is closed so
the
Applicantssheet stays a live pipeline rather than a historical dump.
Related
Convert long email threads into a summary note
Collapse a thread's history into a Doc for handover — perfect for client transitions or vacation cover.
Updated Jun 6, 2026
Pull event RSVPs from emails into a Sheet
Parse yes/no replies to event invites and tally attendance automatically.
Updated Jun 2, 2026
Turn forwarded emails into project tasks
Forward to [email protected] and a row lands in the Projects sheet under the right client.
Updated May 30, 2026
Turn starred emails into a task list
Sync every starred thread into the Northwind Tasks sheet automatically.
Updated May 26, 2026
Alert when a label hits a backlog threshold
Warn the Northwind team in Slack when a Gmail label has more than N unread threads.
Updated Mar 31, 2026