appscript.dev
Automation Beginner Calendar

Schedule personal habits and routines

Block recurring habits on Awadesh's calendar — gym, walks, deep-work mornings.

Published Nov 5, 2025

Awadesh’s habit tracker app keeps a faithful log of streaks but it lives in a silo — colleagues at Northwind cannot see the morning walk window, and the gym slot never collides with a meeting until somebody books over it. The fix is mundane: put the habits on the same calendar everyone else can see.

This script defines a small list of habits in code — gym, walks, deep-work mornings — and writes them onto your calendar for the next few weeks. The schedule lives in source, not in a separate app, so changing “gym Tuesdays” to “gym Mondays” is a one-line edit followed by another run of the script.

What you’ll need

  • Edit access to a Google Calendar. The script uses your default calendar; if you keep personal events on a separate calendar, swap to getCalendarById.
  • A clear idea of the habits you want to block. Adjust the HABITS array below — dayOfWeek is 0 (Sunday) through 6 (Saturday), hour is 24-hour.
  • Nothing else.

The script

// How many weeks ahead to fill. Four weeks is far enough to see the
// shape of the routine without flooding the calendar.
const DEFAULT_WEEKS_AHEAD = 4;

// The recurring blocks to create. Each habit declares which days of the
// week to repeat on, what time to start, and how long it runs.
// dayOfWeek: 0 = Sunday, 1 = Monday, ... 6 = Saturday.
const HABITS = [
  { title: 'Walk',      dayOfWeek: [1, 2, 3, 4, 5], hour: 8,  durationMinutes: 30 },
  { title: 'Gym',       dayOfWeek: [2, 4],          hour: 18, durationMinutes: 60 },
  { title: 'Deep work', dayOfWeek: [1, 3, 5],       hour: 9,  durationMinutes: 120 },
];

/**
 * Walks the next `weeksAhead` weeks and creates a calendar event for
 * every habit on the days it falls on. Existing events with the same
 * title and start time are skipped, so re-runs are safe.
 *
 * @param {number} [weeksAhead] How many weeks ahead to schedule.
 */
function scheduleHabits(weeksAhead = DEFAULT_WEEKS_AHEAD) {
  const cal = CalendarApp.getDefaultCalendar();

  // 1. Anchor "today" at midnight so the day-of-week comparison is
  //    independent of the trigger's clock time.
  const today = new Date();
  today.setHours(0, 0, 0, 0);

  let created = 0;
  for (let d = 0; d < weeksAhead * 7; d++) {
    // 2. Step one day at a time. Easier to reason about than nested
    //    week/day loops, and the cost is negligible.
    const day = new Date(today);
    day.setDate(today.getDate() + d);

    for (const habit of HABITS) {
      if (!habit.dayOfWeek.includes(day.getDay())) continue;

      // 3. Build start and end times for this habit on this day.
      const start = new Date(day);
      start.setHours(habit.hour, 0, 0, 0);
      const end = new Date(start.getTime() + habit.durationMinutes * 60_000);

      // 4. Guard against duplicates. If the same habit is already on
      //    the calendar at this start time, skip it.
      if (alreadyScheduled(cal, habit.title, start)) continue;

      cal.createEvent(habit.title, start, end);
      created++;
    }
  }

  Logger.log('Created ' + created + ' habit blocks.');
}

/**
 * Returns true if an event with the same title already starts within
 * the same minute. The minute granularity is enough to detect the
 * re-run case without false positives across the day.
 */
function alreadyScheduled(cal, title, start) {
  const end = new Date(start.getTime() + 60_000);
  return cal.getEvents(start, end, { search: title }).length > 0;
}

How it works

  1. HABITS is the source of truth for the routine — a tiny array at the top of the file. Each entry says which days, what time, and for how long.
  2. scheduleHabits anchors today at midnight, then walks one day at a time across the window. For each day it loops over the habits and skips any whose dayOfWeek list does not include the current day.
  3. When a habit applies, the script builds a fresh start time on a copy of the date and computes the end time by adding the duration in milliseconds.
  4. Before creating, alreadyScheduled checks whether an event with the same title already starts in the same minute. That makes the script idempotent — re-runs do not pile up duplicates.
  5. cal.createEvent writes the block. Calendars render the title and time exactly the same as any manual event.

Example run

With the default HABITS array and a four-week window starting on Monday 9 June 2026, the script produces blocks like these for the first week:

DayHabitTime
Mon 9 JunWalk08:00 - 08:30
Mon 9 JunDeep work09:00 - 11:00
Tue 10 JunWalk08:00 - 08:30
Tue 10 JunGym18:00 - 19:00
Wed 11 JunWalk08:00 - 08:30
Wed 11 JunDeep work09:00 - 11:00
Thu 12 JunWalk08:00 - 08:30
Thu 12 JunGym18:00 - 19:00
Fri 13 JunWalk08:00 - 08:30
Fri 13 JunDeep work09:00 - 11:00

A second run an hour later writes nothing — every block already exists with a matching title and start time, so alreadyScheduled returns true each time.

Trigger it

The natural cadence is monthly, so you always have four weeks of routine on the calendar:

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

You can equally run it by hand whenever you tweak the HABITS array — the idempotence check means there is no penalty for running it twice.

Watch out for

  • The duplicate check looks for events with the same title starting in the same minute. If you ever rename a habit, the script will see the renamed blocks as missing and add new ones beside them. Rename in the UI as well, or expect a small clean-up.
  • Habits land at the same clock time every day regardless of clashes. The script does not look at your existing meetings — if you want a “first free hour after 09:00” behaviour, search the calendar for the window first.
  • All blocks go on your default calendar. Personal habits on a shared work calendar can leak more context than you mean to. Move them to a separate calendar and update the getDefaultCalendar() call.
  • The dayOfWeek numbers follow JavaScript: 0 = Sunday, not Monday. The most common bug here is off-by-one — a Monday walk that mysteriously runs on Tuesday usually means you wrote 1 thinking “Tuesday”.

Related