Skip to content

GLPI Calendar Sync

Publishes GLPI schedules to external calendar applications (Outlook, Exchange, Google Calendar, Apple Calendar, Thunderbird) via iCalendar feeds and invites.

Work planned in GLPI often needs to be visible in external calendars. Without syncing, scheduled maintenance windows or assigned tasks are easy to overlook when planning other commitments.

Two delivery methods are provided:

Subscription feed Email invitation
How one secret URL per technician, webcal:// or https:// text/calendar; method=REQUEST in the message
Clients Outlook “Subscribe from web”, Google “From URL”, Apple, Thunderbird Outlook, Gmail, Apple Mail — with Accept and Decline
Good for a whole schedule, always current “you’ve been assigned this window”, the moment it happens
Weakness the client refreshes when it wants — Outlook can lag hours one event per message; needs UID/SEQUENCE discipline

Not the Microsoft Graph or Google Calendar APIs: those need an application registration and admin consent per tenant, a stored refresh token per user, and between them they still only cover two providers. iCalendar covers everything.


GLPI ships an ICS export at front/planning.php?genical=1 and a CalDAV server.

The CalDAV server does not answer this problem at all: Exchange and Gmail are not CalDAV clients.

The ICS export is real, and thin:

Core export This plugin
Event identity Ticket#12 — not unique between instances, so two GLPIs overwrite each other in one calendar planning-tickettask-12@your.host, stable across polls
DTSTAMP absent the row’s own modification time, so an unchanged feed is byte-identical
Calendar name none — the subscription is listed under its URL, which is a secret X-WR-CALNAME and NAME
Refresh hint none, so some clients default to daily REFRESH-INTERVAL and X-PUBLISHED-TTL
Reminders none opt-in VALARM per subscription
Free/busy everything marks you busy informational events are TRANSPARENT
Delivery Content-Disposition: attachment inline, with ETag/Last-Modified and 304s
Horizon fixed ±12 months configurable, snapped to midnight so the ETag holds
Change windows not present — a change has no plan dates in core, so it is not a planning type implementation windows, releases and freezes

And the part that is not cosmetic:

Core’s feed authenticates with the user’s personal_token — the same credential that unlocks the REST API. Pasting that URL into Google Calendar hands a third party API access to the instance, and revoking it breaks every other integration sharing the token.

Feeds minted here use their own token, which grants one calendar and nothing else and is revoked on its own.


Chosen per feed, by the person the feed belongs to, from what the administrator permits:

  • My planning — ticket, problem and change tasks, reservations, project tasks, reminders, external events. Sourced through core’s own populatePlanning(), so the visibility rules are core’s rather than a second implementation of them.
  • Change implementation windows — with risk band and collision count in the description. (needs glpi-change)
  • Releases (needs glpi-change)
  • Change freeze periods — all-day and transparent, so a fortnight-long blackout does not mark every technician unbookable. (needs glpi-change)
  • On-call shifts — projected from the rota’s rotation. (needs glpi-signal)

glpi-change and glpi-signal are optional. Every source guards on class_exists(), so this plugin installs and serves feeds on a stock GLPI.


Off on installation. A feed is inert until somebody creates one, so activating this plugin mails nobody; invitations are the opposite, and are an explicit choice rather than a surprise.

Sent when a change acquires or moves an implementation window (to everyone assigned, not to requesters or observers — Accept/Decline implies a say in whether it happens), and when a planned task is assigned to a technician.

The hard part is not sending, it is not sending. item_update fires on every save, and a change is saved many times a day for reasons that have nothing to do with its schedule. Every path is gated on the event actually differing from what was last sent, recorded per event key. Mailing an invitation on every save trains people to ignore them within a week.

Delivery is direct through GLPI’s configured SMTP transport rather than the notification queue — the queue models a message as subject, text, HTML and Document attachments, and there is no hook anywhere in NotificationEventMailing::send() to add a MIME part. The structure is the one Exchange and Google themselves send: multipart/mixed wrapping a multipart/alternative of plain text, HTML and the calendar, plus an .ics attachment for clients that only look for a file.


With intercept_core_feed on (the default), this plugin answers front/planning.php?genical=1 too, so subscriptions already pasted into people’s clients get the better document without anyone editing a URL. Authorisation is core’s, copied unchanged — the same people can read the same plannings.

It upgrades the document. It cannot upgrade the credential: that URL still carries a personal_token. Both the settings page and each user’s own tab say so and point at the replacement.


Preferences → Calendar feeds. Feeds are personal: no screen anywhere shows somebody else’s URL. An administrator can see that a feed exists and revoke it — that is incident response — but never read it.

Revoking purges the row rather than flagging it, because the point of the button is that the URL sitting in Google’s servers stops working.

Terminal window
docker compose -p glpi exec glpi sh -c 'cd /var/www/glpi/plugins/glpicalsync && tests/run.sh'

61 pure assertions over the document format, the invitation rules and token parsing. --with-db additionally exercises a real subscription against the live endpoint and cleans up after itself.