appscript.dev
Automation Advanced Calendar

Auto-reschedule low-priority conflicts

Move flexible Northwind events around fixed ones — focus blocks bend, client calls don't.

Published Oct 16, 2025

A Northwind designer’s week is a mix of two kinds of event: fixed commitments like client calls and reviews, and flexible work like focus blocks and admin time. When a new client call lands on top of a focus block, the focus block should give way — but only a person noticing the clash and dragging it does that. Left alone, the calendar shows a double-booking and the focus block quietly never happens.

This script enforces the rule automatically. Flexible events are marked with a [flex] prefix in their title. The script scans the week, finds any overlap between a flexible event and another event, and slides the flexible one to just after the thing it collided with. Fixed events never move.

What you’ll need

  • The Google Calendar you want managed — the script uses your default calendar.
  • A naming convention: every flexible, movable event has a title starting with [flex], for example [flex] Focus block or [flex] Admin.
  • Nothing else. Anything without the [flex] prefix is treated as fixed and left alone.

The script

// Title prefix that marks an event as flexible (movable).
const FLEX_PREFIX = '[flex]';

// How far ahead to scan for conflicts, in days.
const SCAN_DAYS = 7;

// Gap to leave after a fixed event before a moved flexible one, in minutes.
const BUFFER_MINUTES = 5;

/**
 * Scans the next week, finds overlaps between a flexible event and
 * its neighbour, and shifts the flexible event clear of the conflict.
 */
function rescheduleFlexible() {
  const start = new Date();
  const end = new Date(start.getTime() + SCAN_DAYS * 86400000);

  // Pull the week's events, sorted earliest-start first.
  const events = CalendarApp.getDefaultCalendar()
    .getEvents(start, end)
    .sort((a, b) => a.getStartTime() - b.getStartTime());

  if (events.length < 2) {
    Logger.log('Fewer than two events — no conflicts possible.');
    return;
  }

  // Compare each event with the one immediately after it.
  for (let i = 0; i < events.length - 1; i++) {
    const a = events[i];
    const b = events[i + 1];

    // No overlap: a ends before (or as) b starts — nothing to do.
    if (a.getEndTime() <= b.getStartTime()) continue;

    // They overlap. Move whichever one is flexible; if both are flexible
    // move the later one, if neither is, leave the clash for a human.
    if (a.getTitle().startsWith(FLEX_PREFIX)) {
      shiftAfter(a, b);
    } else if (b.getTitle().startsWith(FLEX_PREFIX)) {
      shiftAfter(b, a);
    }
  }
}

/**
 * Moves the flexible event to start just after the fixed event ends,
 * keeping the flexible event's original duration.
 */
function shiftAfter(flex, fixed) {
  const duration = flex.getEndTime() - flex.getStartTime();
  const newStart = new Date(
    fixed.getEndTime().getTime() + BUFFER_MINUTES * 60000
  );
  flex.setTime(newStart, new Date(newStart.getTime() + duration));
}

How it works

  1. rescheduleFlexible works out a window — now to SCAN_DAYS ahead — and reads every event in it from the default calendar.
  2. It sorts the events by start time, so each event sits next to its nearest neighbour.
  3. If there are fewer than two events there can be no conflict, so it stops.
  4. It walks the list comparing each event a with the next event b. If a ends on or before b starts there is no overlap and it moves on.
  5. When two events overlap it checks the [flex] prefix: if a is flexible it moves a; otherwise if b is flexible it moves b. If neither is flexible the clash is between two fixed events and is left for a person to resolve.
  6. shiftAfter measures the flexible event’s duration, sets its new start to BUFFER_MINUTES after the fixed event ends, and applies a matching new end — so the event keeps its length and just slides later.

Example run

The calendar before a run, on one day:

EventTime
[flex] Focus block10:00 – 12:00
Client call: Acme11:00 – 11:30

The focus block and the client call overlap. After rescheduleFlexible:

EventTime
Client call: Acme11:00 – 11:30
[flex] Focus block11:35 – 13:35

The client call stays put; the focus block keeps its two-hour length and moves to start five minutes after the call ends.

Trigger it

Run this on a daily schedule so the week stays clean as new events land:

  1. In the Apps Script editor open Triggers (the clock icon).
  2. Add a trigger for rescheduleFlexible, Time-driven, Day timer, set for early morning before the working day starts.
  3. Approve the authorisation prompt. Flexible events now get nudged clear of conflicts every morning.

Watch out for

  • The script only compares each event with its immediate neighbour. Moving a flexible event can push it onto a later event — run the trigger again, or loop until no overlaps remain, for dense calendars.
  • A moved flexible event keeps its duration but not its preferred time of day. A morning focus block can land in the afternoon if the day is busy.
  • Only events with the exact [flex] prefix are movable. A typo in the title means the event is treated as fixed and never moved.
  • Two overlapping fixed events are left untouched by design — the script will not choose between two real commitments.
  • The script edits your live calendar with no undo. Test it on a quiet week, or on a secondary calendar, before trusting it with your main one.
  • All-day events have no real start and end time in the usual sense and can produce odd comparisons. Exclude them if your calendar uses them.

Related