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
HABITSarray below —dayOfWeekis 0 (Sunday) through 6 (Saturday),houris 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
HABITSis 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.scheduleHabitsanchors today at midnight, then walks one day at a time across the window. For each day it loops over the habits and skips any whosedayOfWeeklist does not include the current day.- 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.
- Before creating,
alreadyScheduledchecks 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. cal.createEventwrites 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:
| Day | Habit | Time |
|---|---|---|
| Mon 9 Jun | Walk | 08:00 - 08:30 |
| Mon 9 Jun | Deep work | 09:00 - 11:00 |
| Tue 10 Jun | Walk | 08:00 - 08:30 |
| Tue 10 Jun | Gym | 18:00 - 19:00 |
| Wed 11 Jun | Walk | 08:00 - 08:30 |
| Wed 11 Jun | Deep work | 09:00 - 11:00 |
| Thu 12 Jun | Walk | 08:00 - 08:30 |
| Thu 12 Jun | Gym | 18:00 - 19:00 |
| Fri 13 Jun | Walk | 08:00 - 08:30 |
| Fri 13 Jun | Deep work | 09: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:
- In the Apps Script editor, open Triggers.
- 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
dayOfWeeknumbers 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 wrote1thinking “Tuesday”.
Related
Sync birthdays and anniversaries to Calendar
Populate recurring personal dates from a Sheet — the Northwind team rituals calendar.
Updated Nov 1, 2025
Generate recurring events with custom exceptions
Handle complex recurrence rules in code — every Tuesday except UK bank holidays.
Updated Oct 28, 2025
Auto-reschedule low-priority conflicts
Move flexible Northwind events around fixed ones — focus blocks bend, client calls don't.
Updated Oct 16, 2025
Build a contract-renewal calendar
Track Northwind's recurring-revenue renewal dates as calendar events for proactive sales.
Updated Oct 8, 2025
Apply attendee rules by event type
Auto-invite the right Northwind people from a Sheet — design reviews get design, client calls get account leads.
Updated Oct 4, 2025