Blog / Construction Operations
Find Any Project Email in Seconds: Gmail Labels by Job Number
By Keyona Meeks, Founder, ReRev Labs · Oct 31, 2026 · 9 min read
The short answer: Put the job number in the subject line of every project email, in a fixed shape such as
[2417]. Then Gmail can find and label by that number. The no-code route is a filter: search for the number, click Create filter and have Gmail label what matches. The script route reads each new thread's subject and adds a label named for the job. I tested the script with a mock in Node and have not run it in Google, so run the test steps first. Neither route decides what a project email means or whether it must be kept. That is a question for your attorney and your contract.
By the end of this post, you'll be able to:
- Choose a subject line habit that Gmail can sort on.
- Set up a filter that labels by job number.
- Install a script that labels new threads by the number in the subject.
- Know what both routes miss.
I sell build work, and I am not a contractor, a document controller or an attorney. I have no field experience to offer, and nothing here tells you what email you must keep. This is about finding a message you already have.
Why does the subject line matter more than the label?
A label is only as good as the rule that applies it. People will not label every email by hand. They will, with some reminding, put a number in the subject. So the habit goes first: every project email starts with the job number in square brackets, such as [2417] RFI 14 door hardware. Tell subs and the owner's team once, and put it in your first email on each job.
Two cautions. Replies keep the subject, which helps. But a sub who starts a new thread without the number will not be caught. And a number in the subject is not the same as the right job, so a person still checks.
How do you label with a filter and no code?
Google's help page on filters lists these steps: "In the search box at the top, click Show search options," then "Enter your search criteria," then "At the bottom of the search window, click Create filter," then "Choose what you'd like the filter to do," then "Click Create filter." Source: Create rules to filter your emails, read on October 31, 2026.
The page's introduction says filters can "send email to a label." It does not name the option you tick in step four, so I could not confirm where the label choice sits. It also does not say whether a label filter applies to messages already in your mailbox. The only related note I found says that when you create a filter to forward messages, "only new messages will be affected." So test it: after you create the filter, check an older email.
For the search itself, Google's search operators page describes subject: as "Find emails by a word or phrase in the subject line," with the example subject:dinner. Source: Search operators you can use with Gmail, read on October 31, 2026. That page does not say how subject:2417 treats a number inside square brackets, so test subject:2417 against a real thread before you rely on it.
You need one filter per job. That is fine for five jobs and tedious for fifty, which is where the script comes in.
What does the script do?
For each recent thread that has not been checked yet, it reads the subject. If the subject contains a four-digit number in square brackets, it adds a label named Job 2417, creating the label if it does not exist. Either way, it then adds a label named jobchecked, so the thread is not read again.
It looks at the first number only. It handles at most 50 threads per run and looks back three days. All of those are choices I made. They are not Google's limits.
What does Google's documentation say about the methods?
I read the Apps Script reference pages for GmailApp, GmailThread and GmailLabel on October 31, 2026. The pages are long, and the tool I used returned excerpts, so I read the GmailApp page in two parts. The lines the script relies on:
search(query): "Search Gmail with the given query."getUserLabelByName(name): "Retrieves a label given the label name."createLabel(name): "Create a new user label of the given name."getFirstMessageSubject(): "Gets the subject of the first message in the thread."addLabel(label): "Adds this label to the thread."
Sources: Class GmailApp and Class GmailThread.
The script's search uses newer_than:3d and -label:jobchecked. Google's search page describes newer_than: as searching "for emails older or newer than a time period," with d for day, and describes - as "Exclude emails from your search criteria." The same page does not say how a label with spaces is written in a search, which is why I named the check label jobchecked, with no space. I also did not confirm that label:jobchecked matches a label of that name, so the test below checks it.
What is the script?
// Label Gmail threads by the job number in the subject, for example "[2417] RFI 14 door hardware".
// Threads whose subject has no [nnnn] are marked checked and left alone.
const JOB_PATTERN = /\[(\d{4})\]/;
const LABEL_PREFIX = 'Job ';
const CHECKED_LABEL = 'jobchecked';
const SEARCH_QUERY = 'newer_than:3d -label:jobchecked';
const MAX_THREADS = 50;
function labelJobThreads() {
const lock = LockService.getScriptLock();
if (!lock.tryLock(10000)) { return; }
try {
const checked = getOrCreateLabel_(CHECKED_LABEL);
const threads = GmailApp.search(SEARCH_QUERY, 0, MAX_THREADS);
const cache = {};
threads.forEach(function (thread) {
try {
const match = JOB_PATTERN.exec(thread.getFirstMessageSubject());
if (match) {
const name = LABEL_PREFIX + match[1];
if (!cache[name]) { cache[name] = getOrCreateLabel_(name); }
thread.addLabel(cache[name]);
}
thread.addLabel(checked);
} catch (err) {
console.error('Skipped thread ' + thread.getId() + ': ' + err.message);
}
});
} finally {
lock.releaseLock();
}
}
function getOrCreateLabel_(name) {
return GmailApp.getUserLabelByName(name) || GmailApp.createLabel(name);
}
function setupJobLabelTrigger() {
ScriptApp.getProjectTriggers().forEach(function (t) {
if (t.getHandlerFunction() === 'labelJobThreads') { ScriptApp.deleteTrigger(t); }
});
ScriptApp.newTrigger('labelJobThreads').timeBased().everyMinutes(10).create();
}
How do you install it?
- Open the Apps Script editor from a Google Sheet or from script.google.com. I could not confirm a menu path from the pages I read, so follow Google's current help.
- Paste the script and save.
- Run
labelJobThreadsonce. Google asks you to allow the script to use your Gmail. Read the list. Approve it only if you agree. The GmailApp page says its methods need thehttps://mail.google.com/scope, which as I understood it is broad access to the mailbox. Google says scopes are "determined automatically by scanning the script code," which I checked in an earlier review. Think about which account you are giving that to. - Run
setupJobLabelTriggeronce. It removes only its own earlier trigger and adds one that runs every ten minutes. Google's documentation lists 1, 5, 10, 15 and 30 as the allowed values foreveryMinutes, per an earlier review.
Google's documentation on installable triggers says they "always run under the account of the person who created them," per an earlier review. So it labels the mailbox of whoever ran step 4.
How do you test it?
- Send yourself three emails:
[2417] test one, a reply to it, andno number here. - Run
labelJobThreadsby hand. - Check that the first two threads have
Job 2417andjobchecked, and the third has onlyjobchecked. - Run it again. Nothing should change. If the script keeps picking up the same threads, the
-label:jobcheckedsearch is not matching your label, and the script will stall on the same 50. Stop and fix the label name. - Send a fourth email with
[2418]in the subject and confirm a new label appears. - Delete the test emails and the test labels.
What did my mock test cover?
I wrote a Node harness that stands in for Gmail, the lock and the trigger service, and ran 21 checks. They covered: a subject with a bracketed four-digit number gets the job label and the check label; a reply with the same subject matches; a subject with no number gets only the check label; a five-digit number, a number in parentheses and letters in brackets are not matched; only the first number is used when there are two; one label is created for several threads on the same job; existing labels are reused; a second run changes nothing; no more than 50 threads per run, with the rest picked up next time; a thread that errors is logged, left unchecked and does not stop the others; a busy lock skips the run; the lock is released after a failure; and the trigger setup removes only its own trigger and sets ten minutes. All 21 passed.
That proves the logic against my assumptions about Google's behavior. It does not prove the script runs in Google. The mock cannot tell me how Gmail matches the label in a search, how fast the calls run, or what quotas apply to Gmail in Apps Script. I did not find those numbers on the pages I read.
What does the script miss?
- Threads without a number. They get the check label and no job label.
- Subjects that change. The script reads the subject of the first message only.
- Wrong numbers. A typo in the subject files the thread under the wrong job. A bracketed year such as
[2026]would also become a label. - Older mail. It looks back three days, so it will not label history.
- Attachments. It does not save them anywhere.
Should the script archive, forward or delete anything?
No. Version one only adds labels. Anything that moves or removes email is where a small script becomes a risk, and I did not test it.
Where does an LLM fit in this?
An LLM can help with the messy part: reading an email with no job number and suggesting which job it belongs to, from the names and addresses in the text. A person should check that suggestion before the label goes on. The subject check is a plain rule, and it does the same thing every time. My stance: AI writes the code, plain automation runs it, and an LLM shows up only where the input is messy and a person checks the result. For a bigger picture of what to automate first, see what to automate first, and for a primer on the tool behind the script, see what Google Apps Script is.
Where this has limits
- I did not run this script in Google. The mock test used my own stand-ins for Gmail and the trigger service.
- The Google pages were read as excerpts. The GmailApp page was read in two parts, and the GmailLabel page was cut off.
- I could not confirm how a subject search treats square brackets, how a label with spaces is searched, or whether a label filter applies to old mail.
- The trigger, scope and
everyMinuteslines are from an earlier review. Check the live pages. - Gmail quotas for Apps Script are unconfirmed. Keep volume small.
- I am not an attorney, an insurer or a safety professional. Check with an attorney on what email you must keep.
- No ReRev prices and no promises about results.
If you want help setting this up, see our AI operations service or tell us how project email piles up. Do not send emails, contracts, passwords or login details.
Frequently asked questions
Can Gmail label emails by job number automatically?
Yes, with a filter per job or with an Apps Script that reads the subject. This post shows both and tells you what to test.
Do filters apply to old emails?
The Google help page I read does not say for label filters. It says only new messages are affected when you forward. Test with an old email.
Why put the job number in square brackets?
It makes the number easy to match and unlikely to be confused with other numbers. The shape is my suggestion, not a Gmail rule.
Will the script label emails from before I installed it?
No. It looks back three days. Older mail needs a different approach.
Is it safe to give a script access to Gmail?
Read the approval screen. The Gmail methods need broad mailbox access. Run it in an account where you are comfortable with that, and review it before approving.
What if someone forgets the job number?
The thread gets only the check label. A person has to file it by hand.
Read next: Save Gmail attachments to Drive automatically pairs well once emails sort themselves, and the RFI log is where the questions in those emails get tracked.