Recurring Meetings Are Eating Your Rooms. Here Is How to Get Them Back
The 10am Monday standup booked in 2023 for a team that no longer exists is still holding your biggest room every week. Here is how to find every recurring series on your room calendars with PowerShell and the Google Calendar API, the settings that limit the damage, and an audit process that actually survives contact with the org chart.
Walk past your biggest meeting room at 10:05 on a Monday. It is empty. The room calendar says "Platform Standup, 10:00 to 10:30", organised by someone who left in 2024, part of a series created in March 2023 for a team that has been reorganised twice since. Nobody attends it. Nobody cancels it, because nobody is confident it is theirs to cancel. It holds the best room in the office for half an hour, every week, fifty-two weeks a year, and it will keep doing that until a human deliberately intervenes.
That single mechanism is behind most "we need more meeting rooms" conversations in offices of 20 to 100 people. The rooms are not undersupplied. They are pre-committed. This post covers why recurring bookings accumulate and never get cleaned up, how to list every recurring series on your room calendars using Microsoft Graph and the Google Calendar API, which room mailbox settings actually limit the damage, how to run a quarterly recurrence audit that survives contact with the org chart, and where auto-release genuinely helps and where it does not.
Why recurring bookings accumulate
The effort is asymmetric. Creating a weekly series is one action taken by one motivated person. Removing it is a conversation with a person who may not work here any more, about a meeting they may not remember, with a room that is not obviously anyone's responsibility. Booking is cheap and cleanup is expensive, so bookings win.
Three things make it worse:
- No end date is the easy option. Both Outlook and Google Calendar let you create a series that never ends, and both make it the path of least resistance. In Microsoft Graph the recurrence range type is
noEnd. In an RRULE it is simply the absence ofUNTIL=orCOUNT=. - Ownership decays but the booking does not. When the organiser leaves and their mailbox is deleted or converted, the accepted occurrences stay on the room's calendar. The room does not care that the organiser is gone.
- Auto-accept means no human ever reviewed it. Room mailboxes with
AutomateProcessingset toAutoAcceptare doing exactly what you configured them to do, which is to never ask a question. That is the right default. It also means the only review step in the whole lifecycle is the one you have to build yourself.
The arithmetic is worse than it feels
A room has roughly 40 bookable hours in a working week. One recurring weekly hour is 2.5% of that room, permanently. Twenty such series across four rooms is half your capacity gone before anyone books anything ad hoc. This is why adding a fifth room rarely fixes the problem: the new room fills with recurring bookings within a quarter.
Finding them in Microsoft 365
Recurring bookings live on the room mailbox as series master events. Microsoft Graph's GET /users/{id}/events is useful here specifically because, per the event resource documentation, that list "contains single instance meetings and series masters" and does not expand occurrences. That is the opposite of what calendarView does, and it is what you want for an audit.
Each event has a type property with possible values singleInstance, occurrence, exception and seriesMaster. Filter client side on the last one.
Connect-ExchangeOnline
Connect-MgGraph -Scopes "Calendars.Read","User.Read.All"
$rooms = Get-Mailbox -RecipientTypeDetails RoomMailbox -ResultSize Unlimited
$report = foreach ($room in $rooms) {
$events = Get-MgUserEvent -UserId $room.PrimarySmtpAddress -All `
-Property "subject,organizer,start,end,recurrence,createdDateTime,lastModifiedDateTime,type"
foreach ($e in ($events | Where-Object { $_.Type -eq 'seriesMaster' })) {
[pscustomobject]@{
Room = $room.DisplayName
Subject = $e.Subject
Organizer = $e.Organizer.EmailAddress.Address
Created = $e.CreatedDateTime
LastModified = $e.LastModifiedDateTime
Pattern = $e.Recurrence.Pattern.Type
Interval = $e.Recurrence.Pattern.Interval
RangeType = $e.Recurrence.Range.Type
EndDate = $e.Recurrence.Range.EndDate
}
}
}
$report | Sort-Object Room, Created |
Export-Csv .\recurring-room-bookings.csv -NoTypeInformation
The raw REST call, if you would rather not use the PowerShell SDK:
GET https://graph.microsoft.com/v1.0/users/room-oslo@contoso.com/events
?$select=subject,organizer,start,end,recurrence,createdDateTime,type
&$top=100
Prefer: outlook.timezone="Europe/Copenhagen"
Sort the resulting CSV by Created ascending and read the top twenty rows. Those are your problem. Then flag anything where RangeType is noEnd, and check which organisers still exist:
$report.Organizer | Sort-Object -Unique | ForEach-Object {
if (-not (Get-MgUser -Filter "mail eq '$_'" -ErrorAction SilentlyContinue)) {
"ORPHANED ORGANISER: $_"
}
}
An orphaned open-ended weekly series is the easiest deletion you will ever make. Do those first and bank the win before you start any conversations.
Finding them in Google Workspace
Room resources are calendars, so events.list works directly against them. The key parameter is singleEvents. Per the Google Calendar recurring events guide, setting it to true expands the series into individual instances. For an audit you want it false, so you get the underlying recurring event with its recurrence field intact.
function auditRecurringBookings() {
const roomId = 'c_1234567890@resource.calendar.google.com';
let pageToken;
do {
const res = Calendar.Events.list(roomId, {
singleEvents: false,
showDeleted: false,
maxResults: 250,
pageToken: pageToken
});
(res.items || []).forEach(function (e) {
if (!e.recurrence) return;
const rule = e.recurrence.join(' ');
const openEnded = !/UNTIL=|COUNT=/.test(rule);
Logger.log([
e.summary,
e.organizer ? e.organizer.email : 'unknown',
e.created,
rule,
openEnded ? 'OPEN ENDED' : ''
].join(' | '));
});
pageToken = res.nextPageToken;
} while (pageToken);
}
Get the resource calendar IDs from the Admin SDK Directory API (resources.calendars.list) so you do not have to paste them by hand. Run this in Apps Script with the advanced Calendar service enabled and it takes about ten minutes to set up.
Settings that limit the damage
On Exchange Online, Set-CalendarProcessing gives you three levers worth pulling:
BookingWindowInDays: how far ahead the room accepts bookings. Valid values are 0 through 1080, default 180. Dropping contested rooms to 90 means nobody can pin a room a year out.MaximumDurationInMinutes: caps the length of any single booking, including each occurrence of a series.AllowRecurringMeetings: defaults to$true. Setting it to$falseis blunt, and on your one most-contested room it works.
Get-Mailbox -RecipientTypeDetails RoomMailbox -ResultSize Unlimited |
Set-CalendarProcessing `
-BookingWindowInDays 90 `
-MaximumDurationInMinutes 240 `
-ScheduleOnlyDuringWorkHours $true
# The blunt instrument, for the one room everyone fights over
Set-CalendarProcessing -Identity "Room-Boardroom" -AllowRecurringMeetings $false
Two honest caveats. First, BookingWindowInDays does not shorten an existing series. The window rolls forward, so the room keeps accepting new occurrences of an open-ended series indefinitely; you have only limited how far ahead the calendar looks committed. Second, AllowRecurringMeetings $false will generate complaints in week one, and you should expect to have to defend it.
On the Google side there is no equivalent per-resource recurrence switch. You get sharing permissions, booking suggestions and the room release feature, and that is roughly it. Say that out loud to your stakeholders rather than hunting for a setting that does not exist.
The quarterly recurrence audit
Settings alone will not remove what is already there. Make it a recurring process, ideally owned by whoever owns the room policy rather than by IT alone.
- First working Monday of the quarter, run the export script above.
- Filter to series created more than 180 days ago that have no end date.
- Group by organiser. Send one email per organiser containing only their series. Do not send a company-wide message; those get ignored by everyone equally.
- Make deletion the default. The email says: "these series will be shortened to end on 30 November unless you reply by Friday." Opt-out, not opt-in. This is the single design choice that decides whether the audit works.
- Apply the changes on the deadline. Shorten rather than delete where you are unsure; a series with an end date six months out is no longer a permanent problem.
- Record the before and after booked-hours figure per room, so the next audit has a number attached to it.
For twenty rooms, the first run realistically costs half a day including the follow-up emails. Later quarters take about an hour.
Expiry dates as a standing rule
The audit is remedial. The rule is preventive: every recurring room booking has an end date, maximum six months, no exceptions for anybody. In Outlook that means choosing "End by" instead of "No end date". In Google Calendar it means "Ends on" instead of "Never". In an RRULE it means UNTIL= is always present.
Six months is short enough that a dead meeting dies on its own and long enough that renewing is not annoying. Put it in writing next to your other room rules rather than leaving it as folklore.
Auto-release as the safety net
Google Workspace can automatically release booked meeting rooms when all but one person declines the meeting. Note the supported editions listed on that page: Enterprise Standard and Enterprise Plus, the Education editions, and G Suite Business. If you are on Business Standard, this feature is not available to you.
Microsoft 365 has no equivalent native auto-release on the room mailbox itself. Release-on-no-check-in exists in Teams Panels and in Zoom Rooms check-in, both of which mean buying room hardware from that vendor.
What auto-release does not fix
This matters, because auto-release is frequently sold as the answer to the recurring meeting problem and it is not.
- Declines are not no-shows. The dead 10am standup is not declined by anyone. It sits there accepted, and everyone silently ignores it. Release logic sees a healthy meeting.
- Google explicitly does not release rooms booked directly on the room's calendar, rooms that hold 20 or more people, meetings longer than four hours, meetings starting within the next 30 minutes, or meetings with no guests other than the organiser. The room that holds 20 people is usually the exact room you are fighting over.
- It frees the slot too late. A room released at 09:55 does not help the person who needed to plan a 10:00 client call the previous afternoon.
Auto-release trims the tail. Recurrence audits remove the trunk. You want both, in that order of importance.
The social problem, named plainly
The hardest series to remove is always the one belonging to the most senior person. Some things that help:
- Make it a rule, not a judgement. A six-month expiry applied to everyone is administrative. Cancelling one specific person's meeting is political. Only do the first one.
- Get the sponsor to go first. Ask a member of the leadership team to put an end date on their own standing meeting, visibly, before the audit email goes out.
- Do not put the office manager in the firing line. The deadline email should come from a shared IT or workplace address with a stated policy behind it, not from an individual who then has to justify it in the kitchen.
- Bring a number. "This series has held Room 4 for 78 hours this year" is a very different conversation from "I think this meeting is dead."
You will lose some of these arguments. Clearing 60% of stale series is a good quarter. Aim for that rather than for a clean sheet.
Where a display fits into this
A screen on the wall does not delete anything. What it does is make the waste visible and attributable at the moment it happens, which is what turns an abstract policy discussion into a specific one. When the panel outside the boardroom shows "Platform Standup" and the room has been empty for twenty minutes, the next person who walks past has both the evidence and the meeting name.
That is the buy side of this decision. Lobby is a room display that syncs in real time with Google Calendar and Microsoft 365 room resources, in three forms that share one dashboard: a physical e-ink panel on open-source 7.5-inch e-ink hardware from USD 69 per room bought outright direct from the manufacturer (less at volume, USD 62.99 each at 10 or more), a virtual display that is just a URL in any browser tab, and a room overview board for a lobby screen. Pricing is on our pricing page: free forever for up to three displays with 15-minute e-ink refresh and no credit card, then USD 30 per month billed yearly (USD 360 a year, or USD 50 month to month) for unlimited displays, and USD 60 per month billed yearly (USD 720 a year, or USD 100 month to month) for Pro Unlimited (5-minute refresh, Lobby-hosted e-ink screens, custom branding, QR room feedback, a Slack /book command). Yearly billing saves 40% against the month-to-month rate. Setup takes under ten minutes with no sales call.
What it does not do, plainly: no desk booking or visitor management in the rooms product, no room automation or AV control, no enterprise SSO or SCIM at scale, no MDM on the e-ink hardware, and no on-premises Exchange without a hybrid configuration. It also will not delete a single recurring meeting for you. That part is still a script and a difficult email.
TL;DR
- Most room shortages in small offices are pre-commitment from open-ended recurring series, not genuine undersupply.
- List them with
GET /users/{room}/eventsand filter ontype eq 'seriesMaster'in Microsoft 365, orevents.listwithsingleEvents: falsein Google Workspace. Sort by creation date and start at the oldest. - Delete orphaned series with departed organisers first. No conversation required.
- Set
BookingWindowInDaysto 90 andMaximumDurationInMinuteson contested rooms. ConsiderAllowRecurringMeetings $falseon exactly one room. - Run a quarterly audit where deletion is the default and the organiser has to opt out. This is the design detail that makes it work.
- Standing rule: every recurring room booking has an end date, six months maximum, applied to everyone.
- Auto-release helps at the margins. Google's version needs Enterprise editions, triggers on declines rather than no-shows, and skips large rooms and long meetings. It will not solve this for you.
Related reading
- How to release a meeting room automatically
- The real cost of a ghost meeting
- How to reduce meeting room no-shows
- Meeting room booking policy template
- Using the Seeed reTerminal E1001 as a meeting room display
Sources
- Set-CalendarProcessing (ExchangePowerShell), Microsoft Learn
- event resource type, Microsoft Graph v1.0
- List calendarView, Microsoft Graph v1.0
- Recurring events, Google Calendar API
- Release unused Calendar meeting rooms, Google Workspace Admin Help
- Manage resource mailboxes in Exchange Online, Microsoft Learn