Microsoft Outlook
Read and write Outlook and Microsoft 365 calendars through Microsoft Graph, with Teams meetings and delta sync.
Use this provider for Outlook and Microsoft 365 calendars, on work, school and personal Microsoft accounts. It covers most of what Google does, with three differences you'll feel: Outlook stores recurrence as patterns rather than RRULEs, has no "this and following", and syncs a fixed window of time.
npm install @cal-sdk/core @cal-sdk/microsoftimport { listEvents } from '@cal-sdk/core';
import { microsoft } from '@cal-sdk/microsoft';
const calendar = microsoft({ accessToken: () => getFreshAccessToken() });
const { events } = await listEvents({
calendar,
timeMin: '2026-11-01T00:00:00Z',
timeMax: '2026-12-01T00:00:00Z',
});Credentials
accessToken is a Microsoft Graph token with Calendars.ReadWrite, or Calendars.Read if you only read. calsdk doesn't run the OAuth flow: get tokens from your auth library and pass a function that returns a fresh one. calsdk calls it before every request.
Settings
- accessTokenstring | (() => string | PromiseLike<string>)
- Microsoft Graph access token, or a function that returns a fresh one.
- baseURL?string= 'https://graph.microsoft.com/v1.0'
- headers?Record<string, string | undefined>
- Headers sent with every request.
- fetch?typeof fetch
- A custom fetch, for example to log or record requests.
Not supported
Outlook has no thisAndFollowing, event colors, search or free/busy endpoint. Search throws UnsupportedFunctionalityError, so filter the listed events instead, and calsdk computes free/busy from events. See the comparison for every provider.
How it behaves
Recurrence patterns
Outlook stores recurrence as patterns, not RRULEs. Rules with an exact Outlook pattern are converted both ways. Others, such as RDATE, FREQ=HOURLY or BYMONTHDAY=31 alone, throw UnsupportedFunctionalityError, because saving them would quietly schedule a different set of meetings. One difference shows up when reading: Outlook moves a day a month lacks, such as the 31st, to the month's last day, so that rule reads back as BYMONTHDAY=28,29,30,31;BYSETPOS=-1.
Outlook also keeps no list of excluded dates, only occurrences deleted one by one. calsdk reads those back as EXDATE lines on the series, which costs one extra request per series. To exclude a new date, delete that occurrence instead of adding an EXDATE.
No "this and following"
thisAndFollowing throws UnsupportedFunctionalityError. You can still move a whole series from one of its occurrences: with scope all, the series moves by the same wall-clock distance. On a monthly or yearly series that throws too, since its pattern picks days by date and would land on different ones.
A fixed sync window
Sync uses Graph delta queries, which work over a date range. So a sync covers a fixed window, by default one year back and two years ahead of the first sync, and the cursor keeps it. Changes cover single events, series and changed occurrences. Deleting one occurrence changes its series, so it arrives as an update of the series with a new EXDATE.
Ids and time zones
calsdk asks Graph for immutable ids, so an event keeps its id when someone moves it to another calendar, and the ids you've stored stay valid. Times keep the zone they were scheduled in; Outlook names zones the Windows way ("Pacific Standard Time"), and calsdk turns those into IANA names.
Writing events
Outlook ignores If-Match on deletes, so deleteEvent with ifMatch compares the etag first. That leaves a small gap: a change made between the check and the delete isn't detected.
createConference: true asks for a Teams meeting. Personal accounts, and some work accounts, can't create one through Graph, so on those the event is saved without it and the result carries a warning. Outlook also always notifies attendees, so sendUpdates values other than 'all' return a warning.
Outlook allows one popup reminder per event, and it has no default reminder to go back to. So reminders: null and useDefault: true leave the reminder unchanged, with a warning. To turn it off, pass { useDefault: false, overrides: [] }. Events have no color of their own either, only categories, which carry Outlook's colors. color is ignored with a warning, so use the categories option instead.
Provider options
await createEvent({
calendar,
event,
providerOptions: {
microsoft: { showAs: 'oof', importance: 'high', categories: ['Travel'], transactionId: 'trip-42' },
},
});
await syncEvents({
calendar,
providerOptions: {
microsoft: { timeMin: '2026-01-01T00:00:00Z', timeMax: '2027-01-01T00:00:00Z' },
},
});createEvent and updateEvent
- showAs?'free' | 'tentative' | 'busy' | 'oof' | 'workingElsewhere'
- Overrides
showAson the event. - importance?'low' | 'normal' | 'high'
- categories?string[]
- Outlook category names. Categories carry Outlook's colors.
- transactionId?string
- Makes a create safe to retry: Outlook won't create it twice.
syncEvents
- timeMin?string
- ISO 8601 instant with an offset. Where a first sync's window starts.
- timeMax?string
- ISO 8601 instant with an offset. Where a first sync's window ends.
Metadata
Events carry providerMetadata.microsoft:
| Field | Type and notes |
|---|---|
showAs | string: Outlook's own 'free', 'tentative', 'busy', 'oof', 'workingElsewhere' or 'unknown' |
importance | string |
categories | string[] |
iCalUId | string |