// Gmail · Apps Script

Run a script when a new email arrives in Gmail.

Gmail has no native push trigger for Apps Script. Learn the correct pattern: a time-driven trigger, an unread label search, and a processed-label guard to stay idempotent.

I want my Apps Script to automatically run whenever a new email arrives in my Gmail inbox, without clicking anything manually.

The script

copy · paste · trigger
processIncoming.gs
Apps Script
// Poll for unread emails every 5 min; mark processed to stay idempotent
function processIncoming() {
  var label = getOrCreateLabel('script-processed');
  var threads = GmailApp.search('is:unread -label:script-processed', 0, 50);

  for (var i = 0; i < threads.length; i++) {
    var thread = threads[i];
    var messages = thread.getMessages();
    var latest = messages[messages.length - 1];

    Logger.log('Processing: ' + latest.getSubject());
    // Your logic here — forward, log to Sheet, create Calendar event, etc.

    thread.addLabel(label);
    thread.markRead();
  }
}

function getOrCreateLabel(name) {
  var label = GmailApp.getUserLabelByName(name);
  return label ? label : GmailApp.createLabel(name);
}

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

Walkthrough

Why there is no "on new email" trigger

Apps Script exposes a small set of simple triggers — onOpen, onEdit, onFormSubmit — and a broader set of installable ones. Gmail is conspicuously absent from both lists. Google has never shipped a push trigger that fires the moment a message lands. The closest thing is a Gmail add-on's contextual trigger, which only fires when the user opens a specific message in the UI. That is not what you want when you need headless automation.

The real answer is a time-driven trigger set to run every 1 or 5 minutes, paired with a search query that acts as a queue. GmailApp.search() accepts the same operators as the Gmail search bar — is:unread, from:[email protected], subject:invoice, has:attachment — so you can scope the queue tightly. The polling delay is real: if your trigger fires every 5 minutes, worst-case latency is 5 minutes. For most automation that is fine; for true real-time you need a different tool (Pub/Sub via Cloud Functions, or a third-party webhook service).

I keep the polling interval at 5 minutes in production. Every 1 minute sounds better, but Apps Script counts trigger executions against a daily quota (6 minutes of CPU for free accounts, 30 for Workspace). A tight loop on a busy inbox burns that quota fast.

The processed-label guard — why it matters

The first time I skipped the label guard, I processed 300 emails twice because the trigger fired again before the previous run finished marking threads read. GmailApp.search() with is:unread returns a snapshot at query time; if your processing takes more than a few seconds, a concurrent run sees the same unread threads.

The pattern here is defensive: apply a custom label (script-processed) to each thread immediately after handling it, and exclude that label from the search query with -label:script-processed. This makes the operation idempotent — running the function ten times produces the same outcome as running it once. Labels survive across runs, survive script errors, and survive you manually marking something read. They are the right durable marker.

One gotcha: GmailApp.getUserLabelByName() returns null if the label does not exist yet, which throws on the first ever run. The getOrCreateLabel helper shown above handles that. Run it once manually before installing the trigger so the label exists and you can see it in the Gmail sidebar.

Installing the time-driven trigger

Open the Apps Script editor at script.google.com, paste the code, and save. Then go to Triggers (the alarm-clock icon in the left rail) and click Add Trigger. Set the function to processIncoming, event source to Time-driven, type to Minutes timer, and interval to Every 5 minutes. Save.

Apps Script will ask you to authorize the script the first time it runs. It needs GmailApp scope, which covers reading, labeling, and marking messages. If you are on a Google Workspace account, your admin may have restricted which OAuth scopes installable triggers can request — check with them if the authorization dialog fails silently.

To confirm it is working, wait one polling cycle, then open View > Logs in the editor. You should see Processing: lines for any unread messages that matched your search query. After that, check that the script-processed label has appeared on those threads in Gmail.

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
Can I trigger Apps Script the instant an email arrives, with zero delay?
No, not from Apps Script alone. The minimum polling interval is 1 minute, and Apps Script has no webhook receiver. If sub-minute latency matters, use a Gmail Pub/Sub push subscription (via Google Cloud) to hit an HTTPS endpoint, and call the Apps Script Execution API from there.
Why does my script keep processing the same emails over and over?
The processed-label guard is either missing or not being applied before the next trigger fires. Make sure thread.addLabel(label) runs inside your loop, and that your search query includes -label:script-processed. If the script throws before reaching addLabel, those threads stay in the queue.
GmailApp.search() is only returning 50 threads — I have more unread messages than that.
The third argument to GmailApp.search() is the max results cap, which defaults to 500 but can be set explicitly. The second argument is the start offset for pagination. For a large backlog, run a one-time catch-up loop paginating with offset 0, 50, 100, etc. until the result array is empty, then let the recurring trigger handle new arrivals from a clean state.
Does the script run when I send an email, or only when I receive one?
The is:unread search matches received messages in your inbox by default. Sent messages are in the Sent label and are never unread. If you want to act on sent mail too, add a separate search for in:sent after:date or use a dedicated Sent label filter.
// one good script a week

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