How to Generate Offer Letters from a Google Sheets Hiring Tracker (2026)

Hiring trackers already hold the fields an offer letter needs: name, role, start date, compensation, manager, reply-by date. The slow part is copying those fields into a letter, one candidate at a time, and saving a PDF. You can keep the tracker as the source of truth and generate the letter from a template when a row is actually ready.

This is not legal advice, and the script does not write your terms for you. Put language your organization (and, where appropriate, a lawyer) has already approved into the Google Doc, and only merge the fields that change.

Step 1: a hiring sheet with a Ready flag

Use columns such as: Candidate, Email, Role, Team, Start date, Salary, Manager, Offer deadline, Status, plus empty Offer Doc and Offer PDF columns. Status should distinguish Draft, Ready, Sent, Accepted and Declined (or Held). The script below generates documents only for Ready rows, so a half-finished offer is not exported by mistake.

Format dates and currency in the sheet. Helper columns such as =TEXT(E2,"MMMM d, yyyy") for the start date and a similarly formatted salary keep the letter readable.

Treat the sheet as sensitive. Share it with the smallest group you can, and avoid putting extra personal data in columns you do not need to merge.

Step 2: the letter template

Write the offer in Google Docs the way you already would, and replace the variable parts with placeholders that match your headers exactly, for example {{Candidate}}, {{Role}}, {{Start date}}, {{Salary}}, {{Manager}} and {{Offer deadline}}. Keep the legal paragraphs, at-will language, contingencies and signature block as ordinary text in the Doc so they are not coming from a spreadsheet cell.

Copy the template ID (between /d/ and /edit) and the ID of a restricted Drive folder for finished offers (after /folders/). That folder's sharing settings are what control who can see generated letters.

Step 3: generate letters for Ready rows only

In the tracker, open Extensions › Apps Script, paste the script, fill in the two IDs, save, reload, and use Merge › Create offer letters for ready rows. Approve permissions the first time. Rows that already have an Offer PDF link are skipped.

/**
 * Generate an offer letter Doc + PDF for rows whose Status is "Ready"
 * and whose "Offer PDF" cell is empty. Ignores Draft / Held / Accepted rows.
 * Menu: Merge > Create offer letters for ready rows.
 */
const TEMPLATE_ID = 'PASTE_TEMPLATE_DOC_ID';
const FOLDER_ID   = 'PASTE_OUTPUT_FOLDER_ID';

function onOpen() {
  SpreadsheetApp.getUi().createMenu('Merge')
    .addItem('Create offer letters for ready rows', 'mergeReadyOffers')
    .addToUi();
}

function mergeReadyOffers() {
  const sh = SpreadsheetApp.getActiveSheet();
  const data = sh.getDataRange().getDisplayValues();
  const raw  = sh.getDataRange().getValues();
  const head = data[0];
  const statusCol = head.indexOf('Status');
  const pdfCol = ensureCol_(sh, head, 'Offer PDF');
  const docCol = ensureCol_(sh, head, 'Offer Doc');
  const folder = DriveApp.getFolderById(FOLDER_ID);

  for (let r = 1; r < data.length; r++) {
    if (String(raw[r][statusCol] || '').toLowerCase() !== 'ready') continue;
    if (sh.getRange(r + 1, pdfCol).getValue()) continue;
    const rec = {};
    head.forEach((h, i) => { if (h) rec[h] = data[r][i]; });
    const name = 'Offer-' + (rec['Candidate'] || rec['Role'] || ('row-' + (r + 1)));
    const copy = DriveApp.getFileById(TEMPLATE_ID).makeCopy(name, folder);
    const doc  = DocumentApp.openById(copy.getId());
    const body = doc.getBody();
    Object.keys(rec).forEach((h) => {
      const escaped = h.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      body.replaceText('\\{\\{' + escaped + '\\}\\}', rec[h] || '');
    });
    doc.saveAndClose();
    const pdf = folder.createFile(copy.getAs(MimeType.PDF).setName(name + '.pdf'));
    sh.getRange(r + 1, docCol).setValue(copy.getUrl());
    sh.getRange(r + 1, pdfCol).setValue(pdf.getUrl());
  }
}

function ensureCol_(sh, head, name) {
  let i = head.indexOf(name);
  if (i === -1) {
    i = head.length;
    sh.getRange(1, i + 1).setValue(name);
    head.push(name);
  }
  return i + 1;
}

Caveats

Want documents without maintaining a script? SheetWeave (disclosure: we make it) is a Google Sheets add-on that fills a Google Docs template from each row and saves a Doc, a PDF, or both in a Drive folder you choose, writing the links back to the row. It can run on selected rows, all rows, or only new ones. Sheet data stays inside your Google account and it uses narrow scopes. 14-day free trial, no credit card required.
See SheetWeave

Related Google Sheets guides

Instructions refer to Google Sheets, Google Docs, Google Forms, Google Drive and Apps Script as they worked on November 10, 2026. Not affiliated with Google or any other vendor named here. Google Sheets, Google Docs, Google Forms, Google Slides and Google Drive are trademarks of Google LLC. Features change, so verify in your own account. Questions or corrections: hello@loomwise.app.