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
| Resource | Name / value | Purpose |
|---|---|---|
| Sheet | Gmail Log | Columns Subject, From, Date, Snippet |
| Gmail label | logged | Marks threads already written to the sheet |
| Trigger | Time-driven every 15 minutes | Calls 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);
}
}- Line 3: SEARCH_QUERY controls exactly which Gmail threads get pulled in, so keep it narrow to avoid logging unrelated mail.
- Line 8: Getting or creating the 'logged' label means the script always has a marker to apply, even on the very first run.
- Line 9: GmailApp.search caps at 50 threads per call here, which keeps each run fast enough for a 15-minute trigger.
- Line 15: Trimming the snippet to 140 characters keeps the sheet readable instead of dumping an entire email body into one cell.
- Line 16: appendRow writes one row per message, so a thread with three replies produces three separate rows.
- 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
- 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.
- 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.
- 03
Authorize once
Run the main function from the editor. Accept OAuth scopes when prompted — triggers cannot run until authorization succeeds once.
- 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.