appscript.dev
Automation Intermediate Calendar Gmail

Flag meetings that could have been emails

Detect short, agendaless, oversized meetings — the smell of bad calendar hygiene.

Published Oct 12, 2025

Half of Northwind’s most frustrating meetings have the same shape — fifteen minutes long, six people in the room, no agenda in the description, and a title like “Quick sync”. They eat time in aggregate even though none of them look expensive on their own. The first step to retiring them is to count them.

This script scans the last seven days of your calendar and picks out events that match all three “could-have-been-an-email” smells: short, big group, no agenda. The output is a single email each Monday morning listing them — small enough to act on, specific enough that the conversation about “fewer meetings” stops being vague.

What you’ll need

  • Edit access to a Google Calendar (the script uses your default calendar).
  • A mailbox to receive the digest. Edit DIGEST_RECIPIENT below or use Session.getActiveUser().getEmail() to send it to yourself.
  • Nothing else.

The script

// Inbox that receives the weekly digest of suspect meetings.
const DIGEST_RECIPIENT = '[email protected]';

// Meetings shorter than or equal to this many minutes count as "short".
const SHORT_MEETING_MINUTES = 15;

// Meetings with at least this many guests count as "oversized".
const LARGE_MEETING_GUESTS = 4;

// Descriptions shorter than this many characters count as "no agenda".
const MIN_AGENDA_LENGTH = 20;

// How many days back to look. One week is the natural reporting window.
const LOOKBACK_DAYS = 7;

/**
 * Scans the last `LOOKBACK_DAYS` of the default calendar and emails a
 * digest of meetings that are short, large, and agendaless. Quietly
 * does nothing if no meetings match.
 */
function flagWastefulMeetings() {
  // 1. Build the lookback window. Multiplying milliseconds is fine for
  //    a week — no DST off-by-one risk worth worrying about here.
  const start = new Date(Date.now() - LOOKBACK_DAYS * 86_400_000);
  const end = new Date();

  const events = CalendarApp.getDefaultCalendar().getEvents(start, end);

  // 2. Filter on the three smells together. Any one of them alone is
  //    fine — together they describe the meetings worth flagging.
  const flagged = events.filter((e) => {
    const minutes = (e.getEndTime() - e.getStartTime()) / 60_000;
    const guests = e.getGuestList().length;
    const description = (e.getDescription() || '').trim();
    const hasAgenda = description.length > MIN_AGENDA_LENGTH;

    return minutes <= SHORT_MEETING_MINUTES
      && guests >= LARGE_MEETING_GUESTS
      && !hasAgenda;
  });

  if (flagged.length === 0) {
    Logger.log('No wasteful meetings detected this week.');
    return;
  }

  // 3. Format one line per meeting. Date plus title plus headcount is
  //    enough context to recognise the meeting at a glance.
  const tz = Session.getScriptTimeZone();
  const body = flagged
    .map((e) => {
      const when = Utilities.formatDate(e.getStartTime(), tz, 'd MMM HH:mm');
      const headcount = e.getGuestList().length;
      return when + '  ' + e.getTitle() + ' (' + headcount + ' ppl)';
    })
    .join('\n');

  const subject = flagged.length
    + ' meeting' + (flagged.length === 1 ? '' : 's')
    + ' could have been emails';
  GmailApp.sendEmail(DIGEST_RECIPIENT, subject, body);
}

How it works

  1. The script defines what “wasteful” means in three constants at the top — short, large, and agendaless. Move them up or down to tune the signal.
  2. getEvents reads every event whose interval touches the last seven days. That includes recurring instances, which are flagged just like any other event.
  3. The filter combines all three rules. A 30-minute meeting passes the guest-and-agenda checks but fails the length check, so it is not flagged — that is by design.
  4. If nothing matches, the script logs and exits. No “all clear” email, because the inbox already has plenty of clutter.
  5. Otherwise it formats one line per meeting — date, time, title, headcount — and sends a digest to DIGEST_RECIPIENT. The subject line counts the matches, so the inbox preview is already informative.

Example run

A typical week at Northwind might produce a digest like this:

Subject: 3 meetings could have been emails

8 Oct 09:30  Quick sync (5 ppl)
9 Oct 14:00  Status update (6 ppl)
11 Oct 11:15  Touch base (4 ppl)

Each line is recognisable enough to pull up the event and decide whether next week’s version should be cancelled, replaced with a written update, or attended with a sharper agenda.

Trigger it

A Monday-morning digest fits the human rhythm best:

  1. In the Apps Script editor, open Triggers.
  2. Add a trigger for flagWastefulMeetings, event source Time-driven, type Week timer, day Monday, time 8am to 9am.

Run it on demand the first few times to tune the thresholds — what counts as “short” or “oversized” varies by team.

Watch out for

  • The thresholds are deliberately conservative. If the digest is full of legitimate stand-ups, raise LARGE_MEETING_GUESTS to five or six, or shorten SHORT_MEETING_MINUTES to ten. Tune until the list is small enough to act on.
  • “No agenda” is detected by description length, which is crude. A meeting with the description “Project kickoff” looks agendaless because it is shorter than 20 characters. Add a richer check — for example, “contains a newline” — if your team writes terse but real agendas.
  • The script only reads your default calendar. If you want a team-wide view, loop over a list of calendar IDs and concatenate the events.
  • Recurring meetings show up once per instance. A weekly “quick sync” lands in every digest until it is fixed or cancelled — which is arguably the point, but it can feel naggy. Group by event ID if you only want each series flagged once a quarter.

Related