appscript.dev
Automation Intermediate Gmail

Auto-label incoming invoices by sender and amount

Parse the body of incoming invoice emails, tag the thread, and route it to a finance label.

Published Sep 9, 2025

Northwind pays a dozen vendors, and their invoices all arrive by email in slightly different shapes. Awadesh handles finance, but not all invoices are equal: a £40 stationery bill can wait, a £4,000 contractor invoice needs sign-off the same day. Without a way to see size at a glance, every invoice gets the same attention, and the urgent ones queue behind the trivial ones.

This script reads each new invoice email, pulls the amount out of the body with a regular expression, and labels the thread by size bucket — finance/small under £200, finance/medium under £1000, finance/large for the rest. Awadesh opens the large label first and works down. It also stamps a processed label so each invoice is only ever labelled once.

What you’ll need

  • A Gmail account with an arrival filter that applies a finance/incoming label to vendor invoice emails.
  • Three bucket labels created up front: finance/small, finance/medium, and finance/large.
  • A finance/processed label so handled threads are skipped on the next run.

The script

// Label your arrival filter applies to incoming invoice emails.
const INCOMING_LABEL = 'finance/incoming';

// Marker label so each thread is processed exactly once.
const PROCESSED_LABEL = 'finance/processed';

// Bucket boundaries, in your invoice currency.
const SMALL_MAX = 200;    // below this -> finance/small
const MEDIUM_MAX = 1000;  // below this -> finance/medium, else finance/large

/**
 * Finds unprocessed invoice threads, reads the amount from the body,
 * and applies a size-bucket label plus the processed marker.
 */
function labelInvoices() {
  // 1. Pull threads that are incoming but not yet processed.
  const threads = GmailApp.search(
    'label:' + INCOMING_LABEL + ' -label:' + PROCESSED_LABEL
  );

  if (!threads.length) {
    Logger.log('No new invoices to label — nothing to do.');
    return;
  }

  // 2. Look up the labels once, before the loop.
  const buckets = {
    small: GmailApp.getUserLabelByName('finance/small'),
    medium: GmailApp.getUserLabelByName('finance/medium'),
    large: GmailApp.getUserLabelByName('finance/large'),
  };
  const processed = GmailApp.getUserLabelByName(PROCESSED_LABEL);

  let labelled = 0;
  for (const thread of threads) {
    const body = thread.getMessages()[0].getPlainBody();

    // 3. Find the first currency figure, e.g. "$1,250.00" or "$ 40".
    const amountMatch = body.match(/\$\s?([\d,]+(?:\.\d{2})?)/);
    if (!amountMatch) continue;  // no amount found — leave it for a human

    // 4. Strip thousands separators and turn the match into a number.
    const amount = parseFloat(amountMatch[1].replace(/,/g, ''));

    // 5. Pick the bucket from the two boundary constants.
    const bucket =
      amount < SMALL_MAX ? 'small' :
      amount < MEDIUM_MAX ? 'medium' : 'large';

    // 6. Apply the bucket label and mark the thread processed.
    thread.addLabel(buckets[bucket]);
    thread.addLabel(processed);
    labelled++;
  }

  Logger.log('Labelled ' + labelled + ' invoice threads.');
}

How it works

  1. labelInvoices searches for threads that carry finance/incoming but not finance/processed, so only fresh invoices are considered.
  2. If nothing matches, it logs and stops.
  3. The four labels are fetched once, before the loop, rather than on every iteration — fewer Gmail calls.
  4. For each thread it reads the first message body and runs a regex that finds the first currency figure, accepting an optional space after the $ and an optional .00 decimal part.
  5. If no amount is found, the thread is skipped — better to leave it for a human than to bucket it wrongly.
  6. Thousands separators are removed so parseFloat produces a clean number, the bucket is chosen against SMALL_MAX and MEDIUM_MAX, and the thread gets both its size label and the processed marker.

Example run

Three invoice emails arrive overnight:

VendorBody mentionsParsed amountLabel applied
Paperworks Ltd”Total: $48.00”48.00finance/small
Cloudhost”Amount due $640”640finance/medium
Bridge Contractors”Invoice total $4,200.00”4200.00finance/large

Each thread also picks up finance/processed, so the next run ignores all three.

Trigger it

Run this on a short timer so invoices are triaged soon after they land:

  1. In the Apps Script editor, open Triggers (the clock icon).
  2. Click Add Trigger.
  3. Choose labelInvoices, event source Time-driven, and a Minutes timer every 10 minutes.

Watch out for

  • The first $N in the body might not be the total. A vendor who writes “save $20 by paying early” before the total will be bucketed on the discount. For finicky vendors, key off a line like Total: $... with a tighter regex.
  • The regex only matches a $ prefix. If Northwind receives invoices in other currencies, widen the pattern or the figures will be skipped entirely.
  • A thread with no detectable amount is left unprocessed forever, so it will be re-scanned on every run. That is intentional — it keeps it visible — but expect those few threads to linger under finance/incoming.
  • Pair with Extract order numbers from confirmation emails to also capture an invoice ID per thread.

Related