← All posts

Apps Script · Automation

Gmail to Google Sheets with Apps Script: Complete Guide

Marianne Gatlabayan shows how to parse Gmail emails into a Google Sheets database using free Apps Script. Copy-paste code, hourly triggers, live examples. Read now.

Marianne Gatlabayan · Sep 24, 2026 · 12 min read

In short: A Google Apps Script running on an hourly trigger can parse any Gmail label into a Google Sheets database with under 50 lines of code. The setup takes an hour, runs on any free Google account, and eliminates copy-paste work for good. Marianne Gatlabayan has used this exact pattern to capture 10,610 media queries across six verticals with zero manual copy-paste.

Key Takeaways

  • The script pulls sender, date, subject, and body from any Gmail label and appends structured rows to a Sheet
  • Setup requires only a Gmail account, a Google Sheet, and one label applied to the target messages
  • Hourly time-driven triggers keep the database current without any manual work
  • The same script pattern scales to job applications, form submissions, RFPs, and lead intake
  • Regex functions inside the script clean signature clutter and extract phone numbers or emails from message bodies

Funnel diagram showing 100 Gmail emails becoming 100 clean rows in a Google Sheet via an Apps Script hourly trigger, with the note "6+ hours saved per week, zero copy-paste, hourly refresh"

Why manual copy-paste from Gmail costs teams 6 hours a week

Gmail counts over 1.5 billion monthly active users. Google Sheets counts over 900 million. Both live in the same Google account, yet most teams still move information from one to the other by hand. Someone opens an email, highlights the sender name, pastes it into a spreadsheet, tabs over, highlights the subject, tabs again, and repeats the cycle a hundred times a week.

The average knowledge worker loses 6+ hours a week to repetitive tasks that a script could handle. In one production workflow Marianne Gatlabayan built for a digital marketing agency, an inbox parser captured 10,610 media queries across six verticals with zero manual copy-paste after setup. The team that used to spend Monday mornings triaging journalist emails now starts the week with a searchable database that populated itself over the weekend.

This guide covers the same approach so any Google account holder can run it on their own inbox by lunch.

Horizontal bar chart showing customer profiling at 51.4%, report creation at 47.1%, and social media tracking at 44.3% as the most-automated marketing tasks in 2026

What can you build with a Gmail to Google Sheets script?

The output is a Google Apps Script that runs on a timer. It reads a Gmail label, pulls sender, date, subject, and body from each thread, and writes one row per email into a Sheet you control. Nothing gets deleted or edited in Gmail. The Sheet becomes a searchable database that keeps updating on its own.

The pattern applies to any inbox that receives structured or semi-structured messages. Common use cases include media queries from journalists that need daily triage, job applications routed to a careers address, contact form submissions when the form provider does not integrate with Sheets natively, newsletter signups from outreach campaigns, and RFPs or quote requests routed to a shared team inbox.

Each of these currently lives in Gmail and works better as a spreadsheet. Once the data lands in Sheets, you can sort, filter, chart, or pipe it into a dashboard. That flexibility is what turns a passive inbox into an active operations layer.

What does the cost of manual copy-paste really look like?

According to marketing automation research, 47% of marketers use automation specifically to improve efficiency in repetitive processes. Report creation ranks second among the most-automated tasks at 47.14% adoption, right behind customer profiling at 51.43%. Only 40% of businesses have automated even 10% of their tasks. The remaining 60% still move data by hand.

The math on a single manual copy-paste job is punishing. Ten minutes per email, 10 emails per day, five days per week, comes out to over eight hours a week. Multiply across a small team and the annual cost runs into thousands of dollars in wages spent on work a script does for free.

That gap is why a script like this is worth building. The payback period is usually less than one week of use, and the script keeps working long after the person who wrote it has moved on.

What do you need before you start?

Three things. A Gmail account works for any personal or Workspace address. The script will need permission to read your inbox during setup, which Google prompts you to approve the first time you run it. This is a standard OAuth flow and can be revoked at any time from your account settings.

You also need a Google Sheet. Create a blank one, name it something like "Inbox Database", and grab its ID from the URL. The ID is the long string between /d/ and /edit. Save that ID somewhere. The script needs it to know which Sheet to write to.

Finally you need a Gmail label. Apply the label to the emails you want captured. The script reads only that label, so nothing else in your inbox gets touched. This gives you full control over what gets captured. Create the label under Settings > Labels or by clicking the label icon on any open message.

What does the full Gmail to Google Sheets Apps Script look like?

Open your Google Sheet. Click Extensions, then Apps Script. A code editor opens in a new tab with a blank myFunction() shell. Delete that shell, paste the code below, and save.

function parseGmailToSheet() {
  const SHEET_ID = 'YOUR_SHEET_ID_HERE';
  const LABEL_NAME = 'to-capture';
  const SHEET_NAME = 'Inbox Data';

  const sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(SHEET_NAME);

  if (sheet.getLastRow() === 0) {
    sheet.appendRow(['Date', 'From', 'Subject', 'Body Preview', 'Thread ID']);
  }

  const label = GmailApp.getUserLabelByName(LABEL_NAME);
  const threads = label.getThreads();

  threads.forEach(thread => {
    const messages = thread.getMessages();
    messages.forEach(msg => {
      const threadId = thread.getId();
      const existing = sheet.getRange('E:E').getValues().flat();

      if (!existing.includes(threadId)) {
        sheet.appendRow([
          msg.getDate(),
          msg.getFrom(),
          msg.getSubject(),
          msg.getPlainBody().substring(0, 500),
          threadId
        ]);
      }
    });
  });
}

The script does five things in sequence. It opens the target Sheet, adds a header row if the Sheet is empty, pulls every thread with the label, walks each thread's messages, and appends any message not already captured. The existing.includes(threadId) check prevents duplicates when the script runs on a schedule.

Replace YOUR_SHEET_ID_HERE with your Sheet ID. Rename to-capture to whatever label name you applied. Save the script, click Run, and grant the permissions Google asks for. Within a few seconds the Sheet fills with rows.

How do you set an Apps Script to run automatically?

A script that only runs when you click Run is not automation. Triggers make it automation. In the Apps Script editor, click the clock icon on the left sidebar. That opens the Triggers panel. Click Add Trigger in the bottom-right corner.

Set Function to parseGmailToSheet. Set Deployment to Head. Set Event source to Time-driven. Set Type to Hour timer. Set Interval to Every hour. Click Save. Google will ask for permissions again on the first scheduled run.

The script now checks Gmail every hour, captures anything new, and appends it to the Sheet. Free Gmail accounts get up to 20,000 script executions per day, which is far more than an hourly trigger uses. Workspace accounts get even higher limits. Marianne Gatlabayan sets these triggers on client projects and forgets they exist until the Sheet fills up with a month of data.

How do you clean email bodies with regex in Apps Script?

Raw email bodies are messy. Signatures, disclaimers, "Sent from my iPhone" footers, and reply chains bloat every row and push the actual message down where nobody sees it. A five-word question can arrive with 400 words of trailing signature nonsense.

Add this cleaning function above parseGmailToSheet:

function cleanBody(body) {
  body = body.replace(/On.*wrote:[\s\S]*/g, '');
  body = body.replace(/--[\s\S]*/g, '');
  body = body.replace(/\s+/g, ' ').trim();
  return body.substring(0, 300);
}

The first line strips reply chain quoting by removing everything from "On [date], [name] wrote:" onward. The second line strips signatures that start with the standard -- separator. The third collapses runs of whitespace into single spaces. The function returns the first 300 characters of the cleaned message, which is almost always the actual content the sender wanted to convey.

Then swap msg.getPlainBody().substring(0, 500) for cleanBody(msg.getPlainBody()) in the appendRow call. Now each row shows the actual message, not a wall of signature text. If your inbox has unusual signature patterns, add more regex replacements to the function. Marianne Gatlabayan has added patterns for common corporate footers, unsubscribe links, and CRM tracking pixels.

How do you extract specific fields like phone numbers from emails?

Sometimes the real value is buried inside the email body. A price quote, a phone number, a project reference number, a client name. Rather than storing the whole body, you can pull those fields into their own columns for filtering and sorting.

Regex handles that too. Add these extraction functions above the main script:

function extractPhone(body) {
  const match = body.match(/(\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/);
  return match ? match[0] : '';
}

function extractEmail(body) {
  const match = body.match(/[\w.-]+@[\w.-]+\.\w+/);
  return match ? match[0] : '';
}

Each function returns the first match found in the body, or an empty string if nothing matches. The phone pattern handles US and international formats with any common separator. The email pattern catches standard user@domain.tld addresses.

Pass the body through these before writing the row. Add two more items to the appendRow array: extractPhone(msg.getPlainBody()) and extractEmail(msg.getPlainBody()). Update the header row to include Phone and Email columns. Now the Sheet has a dedicated Phone and Email column pulled from every message body, ready to filter or copy into a CRM.

What can go wrong and how do you fix it?

Authorization required loop. The trigger fires but nothing happens. The Executions panel shows an authorization error. This means the trigger tried to run before you approved permissions. Run the function manually once from the editor and approve every permission Google shows. The scheduled trigger only works after this initial handshake.

Duplicate rows. The script above checks column E for the thread ID before writing. If duplicates still appear, someone likely cleared or reformatted column E. The check depends on that column holding thread IDs. Repopulate column E from the last known good state or reset the Sheet and let the script rebuild it.

Quota exceeded. Free Gmail accounts allow 20,000 threads read per day via Apps Script. Enterprise workflows above that limit need to batch by date range or upgrade to Workspace. Add a date filter to the label query to limit each run to the past 24 hours if you hit the ceiling.

Nothing captured. The Executions panel shows the script ran but no rows appeared. Confirm the label name matches exactly, including case. Gmail treats "To-Capture" and "to-capture" as different labels. Confirm at least one email actually carries the label by filtering your inbox by that label in Gmail.

What can you unlock with this pattern?

Once emails land in a Sheet, everything else opens up. Sort by sender to see who reached out most this quarter. Filter by keyword in the subject to build a topical view for content research. Pipe the Sheet into Looker Studio for a visual dashboard that leadership can check without opening Gmail. Trigger a Slack notification when a specific sender comes through by adding a webhook call to the script.

Marianne Gatlabayan uses the same base script to populate a live PR pitch tracker, a client feedback log for franchise brands, and a lead intake system for a solar startup. Every one of those started as a five-line variation of the code above. The pattern generalizes because it does one job well: get messages out of Gmail and into structured storage.

The bigger point is that Apps Script sits inside every Google account waiting for someone to give it a job. Learn this one pattern and you have a template for a hundred more workflows. The next time someone on your team spends an afternoon copying data between two Google products, the answer is almost always a version of this script.

Frequently Asked Questions

Is Google Apps Script free?

Yes. Apps Script is included with every Google account at no cost. Free Gmail accounts have quota limits (100 emails per day sent, 20,000 threads read per day). Google Workspace accounts have higher limits. For most personal or small business use cases, the free tier never gets hit.

Can I use this script on a Workspace account without admin approval?

Individual scripts you write for your own account do not require admin approval. Scripts that access other users' data or share across a domain may need admin review depending on your Workspace security settings. Check with your IT team if the first run gets blocked.

How often should the trigger run?

Hourly is the sweet spot for most use cases. Every 15 minutes is available but rarely needed. Daily works for low-volume inboxes like a careers address that receives a handful of applications per day. Match the trigger frequency to how quickly you actually act on the data.

Does the script edit or delete emails?

No. The script only reads emails and writes to the Sheet. It does not send, reply, delete, archive, or modify anything in Gmail. If you want the script to move captured emails to an archive label, that requires an additional line of code you would add yourself.

Can I parse HTML formatted emails?

Yes. Use msg.getBody() instead of msg.getPlainBody() to get HTML. Then strip tags with a regex like body.replace(/<[^>]+>/g, '') before writing to the Sheet. For anything more than basic tag stripping, use a proper HTML parser via a library like Cheerio adapted to Apps Script.

What to read next


About the Author: Marianne Gatlabayan is a Systems Strategist and Industrial Engineer focused on optimizing operations, automating workflows, and driving business growth. With over a decade of experience in analytics, reporting, and process improvement, she helps organizations build scalable systems that turn complexity into measurable results. In her free time, Marianne enjoys discovering and sipping her favorite coffee bean of the week. Connect on LinkedIn.

Sources

Work with me

Have a process like this worth automating?

Tell me about the bottleneck: the report nobody wants to build, the data nobody trusts. I'll come back with a practical way to automate it.