Apps Script example · 7 min read

Forward Gmail Search Results into a Tracking Sheet: Copy-Paste Apps Script Pattern

Working forward gmail search results into a tracking sheet example in Apps Script—copy-paste code, common mistakes, and when to get it built professionally.

GmailSheetsTime-driven trigger

Support inboxes fill up fast, and important messages often need to live somewhere more structured than a Gmail label. This tutorial builds a script that searches Gmail on a schedule and copies the useful fields from each matching thread into a spreadsheet row.

You will use GmailApp.search with a query string so the script only touches the threads you care about, such as everything tagged with a specific label or sent to a shared alias. Each matching message becomes one row with the subject, sender, date, and a short snippet.

The script tracks which threads it has already logged by applying a Gmail label after processing, so a time-driven trigger can run every fifteen minutes without creating duplicate rows in the sheet.

By the end you will have a self-contained function you can attach to a trigger, a sheet that fills itself in, and enough understanding of GmailApp to adapt the query for your own inbox.

Need this built? Hire a Google Apps Script developer →

Sheet / project setup

ResourceName / valuePurpose
SheetGmail LogColumns Subject, From, Date, Snippet
Gmail labelloggedMarks threads already written to the sheet
TriggerTime-driven every 15 minutesCalls the import function

What It Does

The script runs a Gmail search for threads matching a query, such as label:support-inbox is:unread, and loops over every message in every matching thread.

For each message it has not logged before, it appends a new row containing the subject line, the sender's email address, the date the message was received, and a trimmed snippet of the body.

Prerequisites

You need a Google Sheet with a header row already in place and a Gmail label you are comfortable applying automatically, since the script uses that label to mark messages as handled.

The Apps Script project must be bound to the target spreadsheet, and your account needs to grant Gmail and Sheets scopes the first time you run the function manually.

Walkthrough

Open the bound script editor from Extensions, paste in the logGmailToSheet function, and update the SEARCH_QUERY and SHEET_NAME constants to match your inbox and spreadsheet.

Run the function once from the editor to trigger the OAuth consent screen, approve the Gmail and Sheets scopes, then check the sheet to confirm rows appeared for existing matching threads.

Finally, open Triggers from the clock icon in the left sidebar and add a time-driven trigger set to run every 15 minutes so new mail gets logged automatically.

Edge Cases

Threads with many messages can produce several rows in one run, which is expected since the script logs at the message level rather than the thread level.

If GmailApp.search returns zero threads because the query was mistyped, the function simply exits after logging nothing, so double-check the query syntax in the Gmail search bar first.

Testing

Send yourself a test email that matches SEARCH_QUERY, run logGmailToSheet manually, and confirm a single new row appears with the correct subject and sender.

Run the function a second time immediately afterward and verify no duplicate row is created, which confirms the labeling logic is correctly excluding already-processed threads.

Hardening

Wrap the GmailApp and SpreadsheetApp calls in a try/catch block that logs failures to a separate error sheet, since a single malformed message should not silently stop the whole run.

Add a LockService.getScriptLock call at the top of the function to prevent overlapping executions if the trigger interval is shorter than the time a run typically takes.

Variations

Instead of a fixed search query, you could read the query from a cell in the sheet so support staff can adjust which mail gets logged without touching the code.

For teams that need attachments as well as metadata, combine this script with the attachment-saving tutorial so each logged row also links to a Drive folder of the message's files.

logGmailToSheet.gs

logGmailToSheet searches Gmail for unlabeled matches, appends one row per message to the Gmail Log sheet, and applies a 'logged' label so the next run only touches new mail.

// Forward matching Gmail threads into a tracking sheet
function logGmailToSheet() {
  var SEARCH_QUERY = 'label:support-inbox -label:logged';
  var SHEET_NAME = 'Gmail Log';
  var DONE_LABEL = 'logged';

  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
  var label = GmailApp.getUserLabelByName(DONE_LABEL) || GmailApp.createLabel(DONE_LABEL);
  var threads = GmailApp.search(SEARCH_QUERY, 0, 50);

  for (var i = 0; i < threads.length; i++) {
    var messages = threads[i].getMessages();
    for (var j = 0; j < messages.length; j++) {
      var msg = messages[j];
      var snippet = msg.getPlainBody().substring(0, 140).replace(/\n/g, ' ');
      sheet.appendRow([
        msg.getSubject(),
        msg.getFrom(),
        msg.getDate(),
        snippet
      ]);
    }
    threads[i].addLabel(label);
  }
}
  1. Line 3: SEARCH_QUERY controls exactly which Gmail threads get pulled in, so keep it narrow to avoid logging unrelated mail.
  2. Line 8: Getting or creating the 'logged' label means the script always has a marker to apply, even on the very first run.
  3. Line 9: GmailApp.search caps at 50 threads per call here, which keeps each run fast enough for a 15-minute trigger.
  4. Line 15: Trimming the snippet to 140 characters keeps the sheet readable instead of dumping an entire email body into one cell.
  5. Line 16: appendRow writes one row per message, so a thread with three replies produces three separate rows.
  6. Line 23: Labeling the thread after processing is what prevents the same messages from being logged again on the next trigger run.

Deploy this example

  1. 01

    Open Apps Script

    In the bound spreadsheet: Extensions → Apps Script. For standalone projects, create one at script.google.com and link your Sheet by ID.

  2. 02

    Paste and save

    Add a .gs file, paste the code below, rename constants at the top (sheet names, column letters, API property keys), then save.

  3. 03

    Authorize once

    Run the main function from the editor. Accept OAuth scopes when prompted — triggers cannot run until authorization succeeds once.

  4. 04

    Add the trigger

    Triggers → Add trigger → choose the handler function and event (time-driven, on edit, or on form submit). Delete test triggers before production.

Before you run: forward gmail to sheet

  • 1Sheet created with header row for Subject, From, Date, Snippet
  • 2Gmail label for marking processed threads exists
  • 3SEARCH_QUERY tested directly in Gmail's search bar first
  • 4Function run manually once to grant Gmail and Sheets scopes
  • 5Time-driven trigger added and set to the right interval
  • 6Duplicate-prevention verified by running the function twice in a row
  • 7Error handling added around the Gmail and Sheets calls

Frequently asked questions

No, only threads matching SEARCH_QUERY are touched, so a narrow Gmail search query keeps the script focused on the mail you actually want tracked.

Add a LockService lock as described in the hardening section so overlapping runs cannot both process the same unlabeled threads.

Yes, replace the single SHEET_NAME constant with a small routing map keyed by sender domain, similar to the approach used in the move-files-by-name tutorial.

No, Gmail labels are private to your own mailbox and are never visible to anyone else on the thread.

A label is visible directly in Gmail, making it easy to manually re-queue a thread for reprocessing simply by removing the label.

This example caps GmailApp.search at 50 threads per call, which keeps individual trigger runs fast; raise that limit if your trigger interval is longer.

Related examples

Want this wired into your real workflow?

I adapt these patterns to your Sheet structure, APIs, and triggers — deployed in your Google account. Fixed-scope quotes from $500 · free 30-min consult · quote within 24 hours.