appscript.dev
Automation Beginner Calendar

Enforce recurring no-meeting deep-work days

Block focus days automatically — Northwind teammates own Wednesdays.

Published Aug 17, 2025

Northwind tried to protect deep-work Wednesdays the polite way — a Slack announcement, a calendar note, a gentle reminder in the team handbook. Within three weeks the focus day was speckled with “quick syncs” because nobody could see at a glance that the day was claimed. The fix is to make Wednesday look busy on every shared calendar, the same way any other commitment does.

This script walks the next few weeks of your default calendar, finds every Wednesday, and drops an all-day “Deep work day — no meetings” event on each one. It is idempotent — it checks for an existing block before adding another — so you can run it as often as you like and it will only ever fill the gaps.

What you’ll need

  • Edit access to a Google Calendar (the script uses your default calendar).
  • A consistent name for the block — change BLOCK_TITLE below if you prefer “Focus day” or something more on-brand.
  • Nothing else. No API key, no spreadsheet, no add-on.

The script

// The title used on every created event. Used both as the event title
// and as the search term that detects "already blocked" days, so keep
// it consistent once you have run the script.
const BLOCK_TITLE = 'Deep work day — no meetings';

// 3 = Wednesday in the JavaScript Date convention (Sunday is 0).
const FOCUS_DAY_OF_WEEK = 3;

// How many weeks of Wednesdays to block in one run. Eight weeks is a
// comfortable window — short enough to re-run monthly, long enough that
// nobody books "the Wednesday just after the script's horizon".
const DEFAULT_WEEKS_AHEAD = 8;

/**
 * Walks the next `weeksAhead` weeks and creates an all-day deep-work
 * block on every Wednesday that does not already have one.
 *
 * @param {number} [weeksAhead] How many weeks ahead to fill.
 */
function blockWednesdays(weeksAhead = DEFAULT_WEEKS_AHEAD) {
  const cal = CalendarApp.getDefaultCalendar();

  // 1. Start at midnight today so the day comparison is stable no matter
  //    when the trigger fires.
  const start = new Date();
  start.setHours(0, 0, 0, 0);

  let created = 0;
  for (let i = 0; i < weeksAhead * 7; i++) {
    // 2. Walk one day at a time. A simple loop is easier to reason about
    //    than jumping by seven days from "the next Wednesday".
    const d = new Date(start);
    d.setDate(start.getDate() + i);
    if (d.getDay() !== FOCUS_DAY_OF_WEEK) continue;

    // 3. Skip days that already carry the block. `getEventsForDay` with a
    //    search term is cheap and avoids the duplicate-events smell.
    const exists = cal.getEventsForDay(d, { search: BLOCK_TITLE });
    if (exists.length) continue;

    // 4. Create the all-day block. All-day events sit at the top of the
    //    day strip in Google Calendar — that visibility is the whole point.
    cal.createAllDayEvent(BLOCK_TITLE, d);
    created++;
  }

  Logger.log('Created ' + created + ' deep-work blocks.');
}

How it works

  1. blockWednesdays opens your default calendar and normalises “today” to midnight, so the day-of-week check is not skewed by the trigger’s clock time.
  2. It walks one day at a time across the window — weeksAhead * 7 iterations — and skips any day whose getDay() is not Wednesday.
  3. For each Wednesday it calls getEventsForDay with the block title as the search term. If anything comes back, the day is already protected and the script moves on.
  4. Otherwise it creates an all-day event with the same title. All-day events are deliberately chosen over a long timed event because they collapse into a single strip at the top of the day, leaving the timeline itself visually empty for the focus session.

Example run

Suppose today is Monday 18 August 2025 and you run the script with the default eight-week window. The first run creates a block on every Wednesday between 20 August and 8 October:

DateAction
Wed 20 AugCreated “Deep work day — no meetings”
Wed 27 AugCreated “Deep work day — no meetings”
Wed 3 SepSkipped (already blocked)
Wed 10 SepCreated “Deep work day — no meetings”

A second run an hour later does nothing — every Wednesday in the window already has a matching event, so getEventsForDay returns a hit each time and the loop short-circuits.

Trigger it

This is a low-frequency, idempotent job, so a time-driven trigger that fires once a month is plenty:

  1. In the Apps Script editor, open Triggers.
  2. Add a new trigger for blockWednesdays, event source Time-driven, type Month timer, on the first of the month.

If you prefer weekly, set it to Week timer on Sunday — the script will still only create blocks where they are missing, so re-runs are safe.

Watch out for

  • The search match is on the exact BLOCK_TITLE string. If you rename the block in the future, the script will treat the renamed events as missing and cheerfully add a second block alongside them. Pick a title you can live with, or update both places at once.
  • All-day events still show as “free” on the busy/free overlay by default. If colleagues book over the day anyway, mark the event as Busy under Visibility in the calendar UI — Apps Script cannot change that flag retroactively.
  • The default calendar is whichever calendar the script owner counts as default. If you want to block a shared team calendar instead, swap getDefaultCalendar() for getCalendarById('...') and grant the script access to that calendar.
  • The block does not stop people sending you invitations — it simply tells them you are unavailable. Pair it with a polite auto-decline rule if your team needs harder enforcement.

Related