appscript.dev
Automation Intermediate Gmail Sheets

Auto-draft replies from canned-response templates

Match incoming questions to a template and stage a draft reply in Gmail you only need to send.

Published Oct 21, 2025

Half of Northwind’s support@ mail asks the same three questions — opening hours, how to reset a password, where to find an invoice. The answers never change, but someone still retypes them, and the queue waits while they do.

This script keeps the stock answers in a spreadsheet and matches each incoming question to one. When a thread matches a template’s pattern, it stages a draft reply on the thread using that template’s text. The repetitive replies are written before anyone opens the inbox; the team just reviews and sends.

What you’ll need

  • A Templates Google Sheet with three columns: pattern, subject and body. pattern is a regular expression matched (case-insensitively) against the message subject and body; subject and body are the canned reply. The sheet ID goes in the config below.
  • A Gmail support label applied to incoming support threads, by filter or by hand.
  • Nothing else — the script creates the support/drafted label itself.

The script

// Sheet of canned responses: columns pattern, subject, body.
const TEMPLATES_SHEET = '1abcTemplatesSheetId';

// Gmail search that selects threads to consider: unread support mail
// that has not already been drafted.
const SEARCH_QUERY = 'label:support is:unread -label:support/drafted';

// Label applied once a draft is staged, so threads are not drafted twice.
const DRAFTED_LABEL = 'support/drafted';

/**
 * Matches unread support threads against canned-response templates and
 * stages a draft reply for any that match.
 */
function draftCannedReplies() {
  // 1. Load the templates and the threads to consider.
  const templates = readSheet(TEMPLATES_SHEET);
  const threads = GmailApp.search(SEARCH_QUERY);

  if (!templates.length || !threads.length) {
    Logger.log('No templates or no threads — nothing to do.');
    return;
  }

  // Find or create the marker label once, outside the loop.
  const drafted = GmailApp.getUserLabelByName(DRAFTED_LABEL)
    || GmailApp.createLabel(DRAFTED_LABEL);

  for (const thread of threads) {
    // 2. Combine the subject and body into one string to search.
    const msg = thread.getMessages()[0];
    const haystack = `${msg.getSubject()}\n${msg.getPlainBody()}`;

    // 3. Find the first template whose regex matches. No match = skip.
    const tpl = templates.find((t) => new RegExp(t.pattern, 'i').test(haystack));
    if (!tpl) continue;

    // 4. Stage the canned reply as a draft and label the thread.
    thread.createDraftReply(tpl.body, { subject: tpl.subject });
    thread.addLabel(drafted);
  }
}

/**
 * Reads a sheet's first tab into an array of row objects, keyed by the
 * header row.
 */
function readSheet(id) {
  const [h, ...rows] = SpreadsheetApp.openById(id)
    .getSheets()[0]
    .getDataRange()
    .getValues();
  return rows.map((r) => Object.fromEntries(h.map((k, i) => [k, r[i]])));
}

How it works

  1. draftCannedReplies loads the template sheet into an array of objects and searches Gmail for unread support threads that have not been drafted yet. If either list is empty it logs a message and stops.
  2. It resolves the support/drafted label once, creating it on first run.
  3. For each thread it takes the first message and joins the subject and plain-text body into a single haystack string to search.
  4. It runs Array.find over the templates, testing each pattern as a case-insensitive regular expression. The first template that matches wins; if nothing matches, the thread is skipped and left for a human.
  5. When a template matches, createDraftReply stages the template’s body as a draft, overriding the subject line with the template’s subject.
  6. The thread is labelled support/drafted so the next run leaves it alone.
  7. readSheet is a small helper that turns the first sheet tab into row objects keyed by the header row, so templates are read as {pattern, subject, body}.

Example run

The Templates sheet holds:

patternsubjectbody
password|reset|log ?inRe: Resetting your passwordHi, you can reset your password from the Sign in page…
invoice|receipt|billingRe: Finding your invoiceHi, your invoices live under Account > Billing…

A customer emails:

Subject: Can’t log in

Hi, I’ve forgotten my password and can’t get into my account.

The body matches the first template’s pattern (log ?in), so a draft is staged with the subject “Re: Resetting your password” and the password-reset text. The thread gets the support/drafted label. A thread that matches no pattern — say a bespoke complaint — is left untouched for an agent to handle personally.

Trigger it

Run this on a frequent time-based trigger so drafts appear soon after mail arrives:

  1. In the Apps Script editor open Triggers (the clock icon).
  2. Add a trigger for draftCannedReplies, Time-driven, Minutes timer, every 10 minutes.

Ten minutes keeps the common replies waiting in the queue almost as fast as the questions arrive.

Watch out for

  • createDraftReply stages, never sends. Awadesh reviews each one in Gmail before hitting send — a template can land on a thread it does not quite fit, and the human catch is what makes that safe.
  • Template order matters. Array.find returns the first match, so put more specific patterns above broad ones in the sheet.
  • Patterns are real regular expressions. An unescaped . or ( will match more than you expect — test new patterns before relying on them, and escape literal punctuation.
  • The script reads only the first message in the thread, so a follow-up that introduces a new question on an old thread will not be re-matched.
  • A blank or invalid pattern cell will throw when new RegExp is called. Keep the sheet tidy and avoid empty rows below the data.

Related