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/incominglabel to vendor invoice emails. - Three bucket labels created up front:
finance/small,finance/medium, andfinance/large. - A
finance/processedlabel 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
labelInvoicessearches for threads that carryfinance/incomingbut notfinance/processed, so only fresh invoices are considered.- If nothing matches, it logs and stops.
- The four labels are fetched once, before the loop, rather than on every iteration — fewer Gmail calls.
- 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.00decimal part. - If no amount is found, the thread is skipped — better to leave it for a human than to bucket it wrongly.
- Thousands separators are removed so
parseFloatproduces a clean number, the bucket is chosen againstSMALL_MAXandMEDIUM_MAX, and the thread gets both its size label and theprocessedmarker.
Example run
Three invoice emails arrive overnight:
| Vendor | Body mentions | Parsed amount | Label applied |
|---|---|---|---|
| Paperworks Ltd | ”Total: $48.00” | 48.00 | finance/small |
| Cloudhost | ”Amount due $640” | 640 | finance/medium |
| Bridge Contractors | ”Invoice total $4,200.00” | 4200.00 | finance/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:
- In the Apps Script editor, open Triggers (the clock icon).
- Click Add Trigger.
- Choose
labelInvoices, event source Time-driven, and a Minutes timer every 10 minutes.
Watch out for
- The first
$Nin 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 likeTotal: $...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
Convert long email threads into a summary note
Collapse a thread's history into a Doc for handover — perfect for client transitions or vacation cover.
Updated Jun 6, 2026
Pull event RSVPs from emails into a Sheet
Parse yes/no replies to event invites and tally attendance automatically.
Updated Jun 2, 2026
Turn forwarded emails into project tasks
Forward to [email protected] and a row lands in the Projects sheet under the right client.
Updated May 30, 2026
Turn starred emails into a task list
Sync every starred thread into the Northwind Tasks sheet automatically.
Updated May 26, 2026
Alert when a label hits a backlog threshold
Warn the Northwind team in Slack when a Gmail label has more than N unread threads.
Updated Mar 31, 2026