// Gmail · Apps Script

Forward matching emails automatically in Gmail.

Use Apps Script and a Gmail label to auto-forward emails that match a search query — without re-forwarding the same thread on every trigger run.

I want emails matching a specific sender or keyword to be forwarded to another address automatically, without babysitting my inbox.

The script

copy · paste · trigger
forwardMatching.gs
Apps Script
// Forward emails matching QUERY to FORWARD_TO every N minutes.
// Applies a label after forwarding so the thread is excluded next run.
const QUERY = 'from:invoices@supplier.com is:unread';
const FORWARD_TO = 'finance@yourcompany.com';
const DONE_LABEL = 'auto-forwarded';

function forwardMatchingEmails() {
  const label = getOrCreateLabel(DONE_LABEL);
  const threads = GmailApp.search(QUERY + ' -label:' + DONE_LABEL);
  for (const thread of threads) {
    const messages = thread.getMessages();
    for (const msg of messages) {
      GmailApp.sendEmail(FORWARD_TO, msg.getSubject(), msg.getPlainBody());
    }
    thread.addLabel(label);
  }
}

function getOrCreateLabel(name) {
  return GmailApp.getUserLabelByName(name) || GmailApp.createLabel(name);
}

Need a variant? Gnaw writes a custom version from one sentence — fields, triggers, edge cases handled.

Walkthrough

Why the label beats a timestamp

The naive approach is to record a timestamp and only process threads newer than the last run. It sounds clean. In practice it breaks silently: if a thread arrives one second before your trigger fires and the script errors out partway through, that thread falls into a gap between timestamps and never gets forwarded.

A label is idempotent. The search query appends '-label:auto-forwarded', so any thread that received the label is permanently excluded from future runs regardless of when the trigger fires. The first time I hit this gap problem I had forwarded the same invoice three times before I understood why; switching to a label fixed it immediately.

Gmail's search grammar handles the exclusion natively. You are not writing filter logic in code — you are pushing the state into Gmail itself, where it belongs.

Wiring the time-based trigger

Open the Apps Script editor at script.google.com, paste the code, and save. Then go to Triggers (the clock icon on the left sidebar) and add a new trigger: choose 'forwardMatchingEmails', event source 'Time-driven', type 'Minutes timer', interval '5 minutes'. Apps Script will ask for Gmail permissions on the first run.

GmailApp.search returns at most 500 threads per call. If your query could match more than that in a single 5-minute window — bulk newsletters, high-volume alerts — add a slice: pass a second argument like GmailApp.search(query, 0, 50) to cap the batch and let subsequent runs catch the tail.

The sender field in forwarded messages will be your Gmail address, not the original sender. That is a Gmail API constraint, not a script bug. If the recipient needs to know the original sender, prepend it to the body: 'From: ' + msg.getFrom() + ' wrote:\n\n' + msg.getPlainBody().

Adjusting the query without breaking existing labels

QUERY supports the full Gmail search syntax: from:, subject:, has:attachment, is:unread, after:, label:, and boolean operators. You can tighten it at any time without touching the label logic. Threads already labeled 'auto-forwarded' stay excluded regardless of whether the new query would match them.

If you want to re-forward a thread for some reason, open Gmail, find the thread, remove the 'auto-forwarded' label manually, and the next trigger run will pick it up again.

One gotcha worth naming: is:unread in the query means a thread only qualifies while it has at least one unread message. If something else marks the thread read before your trigger fires — a mobile client, another filter — the thread silently falls through. Drop is:unread from the query and rely solely on the label exclusion if you cannot tolerate that miss.

Want a custom version?

Describe your sheet and the rule you want. Gnaw writes the Apps Script — fields, triggers, edge cases — in one shot.

FAQ

4 questions
Will the forwarded email show the original sender's address?
No. Gmail API forwards always come from your account address. The original sender is only available as metadata inside the message — use msg.getFrom() and embed it in the subject or body if the recipient needs it.
What happens if the script runs while a matching thread is still arriving (partial thread)?
The script labels the thread after forwarding all messages present at that moment. If more replies arrive later, the thread already carries the 'auto-forwarded' label and those replies will not be forwarded. If you need to catch all replies, remove is:unread from the query and switch to matching on a subject prefix or sender only, then handle new messages separately.
Can I forward to multiple addresses?
GmailApp.sendEmail accepts a comma-separated string for the first argument: 'alice@example.com,bob@example.com'. Alternatively call sendEmail twice inside the message loop. Both approaches work; the comma-separated form is one line.
The trigger ran but nothing was forwarded — how do I debug it?
Check two things: first, run forwardMatchingEmails manually from the editor and read the execution log (View > Executions). Second, test your QUERY string directly in Gmail search to confirm it returns the threads you expect. The most common cause is -label:auto-forwarded excluding everything because the label was applied during a previous test run.
// one good script a week

Get a working Apps Script snippet in your inbox, weekly.