Blog / Construction Operations

Drawing Transmittal Log That Sends Itself

By Keyona Meeks, Founder, ReRev Labs · Oct 31, 2026 · 10 min read

The short answer: A drawing transmittal log is a list of what you sent, to whom, which revision, and whether anyone answered. A Google Sheet can hold it. One formula shows each line as Open, Overdue or Closed. A short Apps Script then emails you the overdue ones each morning, so nothing waits unnoticed. This is a tracking tool only. It does not replace your contract, and it gives no legal advice. I tested the script with a mock in Node and the formula in LibreOffice. Nothing was run in Google.

By the end of this post, you'll be able to:

  • Set up a transmittal log in a Google Sheet.
  • Show Open, Overdue and Closed on each line.
  • Get a daily email of overdue lines.
  • Know what this log cannot do.

I sell build work, so weigh this post with that in mind. This is a record-keeping aid. Your contract, your project manual and your attorney decide what counts as a valid notice, a deadline or a delivery. Check with an attorney, insurer or safety professional before you rely on any log for those questions.

What is a transmittal log?

A transmittal is a record that a set of documents went from one party to another. A log is the running list of them. For drawings, one line usually says what was sent, which revision, who got it, when, and when an answer is due. I describe this from general practice, not from a standard.

I looked for an official rule that says what a transmittal log must contain, and I did not find one. The one publisher page I read, AIA Contract Documents' page for form G712, Shop Drawing and Sample Record, did not describe what the form records, so I cannot say what it requires. If your contract names a form or a process, follow that. I make no claim that this sheet meets any contract or any rule.

What columns does the log need?

Keep it small. Row 1 holds headings. The tab is named Transmittals.

ColumnHeadingWhat goes in it
ATransmittalYour own number, such as T-001
BDate sentThe day it went out
CToWho received it
DDocumentSheet number or title
ERevRevision
FResponse dueThe date you need an answer by
GReceived backThe date an answer came, left blank until then
HStatusThe formula below

Where the response date comes from is not for this sheet to decide. Use the date your contract or your project manual gives you, or the date you chose. I do not supply a default.

How does the Status column work?

Put this in H2 and fill it down:

=IF(A2="","",IF(G2<>"","Closed",IF(F2="","No due date",IF(F2<TODAY(),"Overdue "&(TODAY()-F2)&" days","Open"))))

A line with an answer date is Closed. A line with no due date shows "No due date," so you notice it. A due date before today shows Overdue and the number of days. A due date of today still shows Open.

I tested this in LibreOffice with five made-up rows: overdue by 5 days, due today, closed, due in 4 days and no due date. They returned "Overdue 5 days," "Open," "Closed," "Open" and "No due date." I did not run it in Google Sheets, so check it there with your own rows.

What does the script look like?

Paste this into the Apps Script project attached to the sheet. It reads the log and changes nothing. Nothing in it runs until you call it.

// Emails a list of transmittals that are past their response date and not marked received. It reads the "Transmittals" tab and changes nothing.
var RECIPIENT = '';       // Paste the address that should get the list. Your decision.
var SEND_HOUR = 7;        // Hour of day, in the script time zone.
var TAB = 'Transmittals';
var MAX_LINES = 40;       // Most lines in the email.
var HEADERS = {           // Change the text on the right to match YOUR column headings.
  id: 'Transmittal', to: 'To', doc: 'Document', due: 'Response due', back: 'Received back'
};

function dayKey(value, tz) {
  if (value === '' || value === null || value === undefined) return '';
  if (Object.prototype.toString.call(value) === '[object Date]') return Utilities.formatDate(value, tz, 'yyyy-MM-dd');
  return String(value).slice(0, 10);
}

function findColumns(headerRow) {
  var idx = {};
  Object.keys(HEADERS).forEach(function (k) {
    var at = headerRow.map(function (h) { return String(h).trim(); }).indexOf(HEADERS[k]);
    if (at === -1) throw new Error('Missing column in ' + TAB + ': ' + HEADERS[k]);
    idx[k] = at;
  });
  return idx;
}

function findOverdue(rows, idx, todayKey, tz) {
  var overdue = [];
  var noDate = 0;
  rows.forEach(function (r) {
    if (String(r[idx.id]).trim() === '') return;
    if (String(r[idx.back]).trim() !== '') return;
    var due = dayKey(r[idx.due], tz);
    if (due === '') { noDate++; return; }
    if (due < todayKey) overdue.push({ id: r[idx.id], to: r[idx.to], doc: r[idx.doc], due: due });
  });
  overdue.sort(function (a, b) { return a.due < b.due ? -1 : a.due > b.due ? 1 : 0; });
  return { overdue: overdue, noDate: noDate };
}

function buildEmail(result, todayKey) {
  var lines = ['Transmittals past their response date, as of ' + todayKey, ''];
  if (result.overdue.length === 0) {
    lines.push('None.');
  } else {
    result.overdue.slice(0, MAX_LINES).forEach(function (o) {
      lines.push(o.id + '  ' + o.doc + '  to ' + o.to + '  (due ' + o.due + ')');
    });
    if (result.overdue.length > MAX_LINES) lines.push('Plus ' + (result.overdue.length - MAX_LINES) + ' more.');
  }
  lines.push('', 'Open with no response date: ' + result.noDate);
  return lines.join('\n');
}

function sendOverdueList() {
  if (!RECIPIENT) throw new Error('Set RECIPIENT first.');
  if (MailApp.getRemainingDailyQuota() < 1) throw new Error('No email quota left today.');
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(TAB);
  if (!sheet) throw new Error('Missing tab: ' + TAB);
  var values = sheet.getDataRange().getValues();
  if (values.length === 0) throw new Error('The ' + TAB + ' tab is empty.');
  var idx = findColumns(values[0]);
  var tz = Session.getScriptTimeZone();
  var todayKey = Utilities.formatDate(new Date(), tz, 'yyyy-MM-dd');
  var result = findOverdue(values.slice(1), idx, todayKey, tz);
  MailApp.sendEmail(RECIPIENT, 'Overdue transmittals: ' + result.overdue.length, buildEmail(result, todayKey));
  return result;
}

function installTrigger() {
  ScriptApp.newTrigger('sendOverdueList')
    .timeBased()
    .everyDays(1)
    .atHour(SEND_HOUR)
    .inTimezone(Session.getScriptTimeZone())
    .create();
}

What the parts do, with Google's own descriptions as I read them in the method tables this session:

  • getActiveSpreadsheet(): "Returns the currently active spreadsheet, or null if there is none." getSheetByName(name): "Returns a sheet with the given name." getDataRange(): "Returns a Range corresponding to the dimensions in which data is present." getValues(): "Returns the rectangular grid of values for this range."
  • Utilities.formatDate "Formats date according to specification described in Java SE SimpleDateFormat class." Session.getScriptTimeZone() "Gets the time zone of the script."
  • MailApp.sendEmail(recipient, subject, body) "Sends an email message." getRemainingDailyQuota() "Returns the number of recipients you can send emails to for the rest of the day."
  • everyDays(n) "Specifies to run the trigger every n days." atHour(hour) "Specifies the hour the trigger at which the trigger runs." inTimezone(timezone) "Specifies the timezone for the specified dates/time when the trigger runs."

It finds each column by the heading in the HEADERS block, so you can rename a heading and change the text there. It stops with a message that names a missing heading. It skips blank lines and lines with an answer date. It lists the oldest overdue line first, up to 40, and says how many open lines have no response date. When nothing is overdue, it still sends a short "None" email, so you know it ran.

Who should get the email?

You decide. RECIPIENT is a paste value, and the script will not run until you set it. Ask the people involved before you add them. The email names recipients and documents, so think about who should see them.

How do you put it on a daily schedule?

Run installTrigger once, then check your project's triggers list for exactly one entry. I could not confirm which minute of the hour a trigger fires, so do not expect 7:00 sharp. Per an earlier review of Google's triggers page, installable triggers "always run under the account of the person who created them." Per an earlier review of Google's authorization page, scopes are "determined automatically by scanning the script code." Read the permission prompt before you approve it.

How do you test it?

  1. Enter five test lines: one overdue, one due today, one answered, one due next week and one with no due date.
  2. Check the Status column against your own reading of each line.
  3. Set RECIPIENT to your own address and run sendOverdueList by hand.
  4. Check that only the overdue line is listed, and that the no-date count is 1.
  5. Change a heading by one letter. You should see the missing-heading message. Fix it.
  6. Run installTrigger and check the triggers list.
  7. Delete the test lines.

What did the test show?

I tested the script in Node with a mock of the Google services. All 15 checks passed. They covered overdue lines listed oldest first, answered, future and due-today lines left out, the no-date count, date cells, a "None" email, a missing heading, an empty tab, a blank recipient, a used-up quota, a missing tab, the 40-line cap, headings with stray spaces and the trigger chain. The mock cannot show how Google returns your dates. That is why steps 1 to 4 matter.

Where does an LLM fit in this?

Not in the log or the script. A date comparison is a plain rule. An LLM can help with the messy side: reading a pile of emails or PDFs and suggesting which transmittal each answer belongs to, or drafting a polite chaser. A person checks every match before changing the log, because an LLM can pair the wrong document and sound sure. 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.

Where this has limits

  • It is a tracking aid, not a legal record. I give no legal advice. Check with an attorney, insurer or safety professional on notice, delivery and deadline questions.
  • I found no official rule for log contents. The AIA page I read did not describe its form.
  • Nothing was run in Google. The script test was a mock in Node. The formula test was in LibreOffice.
  • Anyone who can edit the sheet can change the record. Sheets can keep an edit history, but I did not confirm how, so I make no claim about it.
  • It does not know if a document was received or read. It only knows what someone typed.
  • Email quotas apply. I quote the quota call above. Check Google's limits for your account type.
  • No ReRev prices and no promises about results.

If you want help turning this into a system your whole project team uses, see our AI operations service or tell us how you track documents now. Do not send contracts, drawings, passwords or login details.

Frequently asked questions

What is a drawing transmittal log?

A running list of the documents you sent, to whom, which revision, and whether anyone answered.

Can a Google Sheet be a transmittal log?

Yes, as a tracking aid. Whether it satisfies your contract is a question for your contract and your attorney.

Does the sheet send transmittals?

No. It tracks them and emails you a list of overdue lines. You send the documents another way.

What should the response due date be?

Use the date your contract or project manual gives you, or one you choose. I do not give a default.

Does it use an LLM?

No. An LLM is optional, to help match replies to lines, and a person checks the result.

Can the log prove a document was delivered?

No. It records what someone typed. Ask an attorney what counts as proof under your contract.

Read next: Document control in Google Drive organizes the drawings this log sends out, and the submittal log tracks the review side of the same documents.