appscript.dev
Automation Intermediate Gmail Sheets

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 Applicants sheet with a header row carrying these columns, in this order: receivedAt, name, email, role, stage.
  • A Gmail filter that applies the label careers/new to inbound application emails — for example, anything sent to [email protected].
  • Nothing else to set up: the script creates the careers/acknowledged label 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

  1. processApplications looks up the careers/new label your filter applies, and the careers/acknowledged label — creating the second one if it does not yet exist.
  2. It filters the inbound threads down to those that are not already carrying the careers/acknowledged label, so a thread is only ever handled once. If nothing is left, it logs a message and stops.
  3. For each thread it reads the first message and splits the From header into a display name and an email address with a small regular expression.
  4. It sends the REPLY acknowledgement back on the same thread and adds the careers/acknowledged label immediately, so a mid-run failure cannot cause a double email.
  5. It appends a row to the Applicants sheet with a timestamp, the candidate’s name and email, the role pulled from the subject by parseRole, and a starting stage of received.

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:

receivedAtnameemailrolestage
2026-05-25 09:32Jane Doe[email protected]Senior Designerreceived

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:

  1. In the Apps Script editor, open Triggers (the clock icon).
  2. Click Add Trigger.
  3. Choose processApplications, event source Time-driven, type Minutes timer, and Every 30 minutes.
  4. 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.
  • parseRole depends on the subject containing the word “application”. If your job board sends a different subject line, applicants will be logged as unspecified — adjust the regular expression to match your advert format.
  • thread.reply sends from the account running the script. If careers@ 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 Applicants sheet stays a live pipeline rather than a historical dump.

Related