Quickstart
Create a recurring event, move one occurrence and find free time, with no account and no network.
Start with a calendar you can't break: @calsdk/mock, which keeps events in memory. You'll create a weekday standup, move Wednesday's to later in the morning, and find a free slot around it.
Install
npm install calsdk @calsdk/mockCreate a recurring event
Save this as quickstart.ts:
import { createEvent, findFreeTime, listEvents, updateEvent } from 'calsdk';
import { mockCalendar } from '@calsdk/mock';
const calendar = mockCalendar();
const { event: standup } = await createEvent({
calendar,
event: {
title: 'Standup',
start: { dateTime: '2026-11-09T09:30:00', timeZone: 'Europe/Berlin' },
end: { dateTime: '2026-11-09T09:45:00', timeZone: 'Europe/Berlin' },
recurrence: ['RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR'],
},
});
console.log(standup.kind, standup.start.dateTime);
// series 2026-11-09T09:30:00+01:00You didn't pass calendarId, so the event went to 'primary', the user's default calendar. And because the dateTime had no offset, calsdk read it as 9:30 on the Berlin wall clock and returned it with the offset filled in.
List a week
const { events } = await listEvents({
calendar,
timeMin: '2026-11-09T00:00:00+01:00',
timeMax: '2026-11-14T00:00:00+01:00',
});
for (const event of events) console.log(event.id, event.kind, event.start.dateTime);
// evt_1_20261109T083000Z occurrence 2026-11-09T09:30:00+01:00
// evt_1_20261110T083000Z occurrence 2026-11-10T09:30:00+01:00
// evt_1_20261111T083000Z occurrence 2026-11-11T09:30:00+01:00
// evt_1_20261112T083000Z occurrence 2026-11-12T09:30:00+01:00
// evt_1_20261113T083000Z occurrence 2026-11-13T09:30:00+01:00listEvents expands recurring events by default, so the series comes back as one occurrence per weekday. Each has its own id, which is what you pass to change a single day, and seriesId points back at the series.
Move one occurrence
const wednesday = events[2];
const { event: moved } = await updateEvent({
calendar,
eventId: wednesday.id,
patch: {
start: { dateTime: '2026-11-11T11:00:00', timeZone: 'Europe/Berlin' },
end: { dateTime: '2026-11-11T11:15:00', timeZone: 'Europe/Berlin' },
},
});
console.log(moved.kind, moved.start.dateTime);
// exception 2026-11-11T11:00:00+01:00Updating an occurrence changes only that one by default, and it becomes an exception: an occurrence that no longer follows the series rule. Pass scope: 'all' to change the whole series instead. Recurring events covers the other scopes.
Find free time
const { slots } = await findFreeTime({
calendars: [{ calendar }],
timeMin: '2026-11-09T00:00:00+01:00',
timeMax: '2026-11-10T00:00:00+01:00',
duration: 30,
slotInterval: 30,
workingHours: { start: '09:00', end: '11:00', timeZone: 'Europe/Berlin' },
});
console.log(slots.map((slot) => slot.start));
// [ '2026-11-09T08:00:00.000Z', '2026-11-09T09:00:00.000Z', '2026-11-09T09:30:00.000Z' ]Monday's standup blocks the 9:30 slot, so the free half hours are 9:00, 10:00 and 10:30 in Berlin. Slots come back as UTC instants, which is why 9:00 in Berlin prints as 08:00:00.000Z.
Run it
bun quickstart.ts
# or: npx tsx quickstart.tsAlong with your own output, you'll see this line:
calsdk warning (mock, getFreeBusy): freeBusy uses a fallback: Provider "mock" has no free/busy endpoint, so busy times were computed from events.That's a warning, not an error. The mock has no free/busy endpoint, so calsdk worked out busy times from its events and tells you it did. calsdk does the same for Outlook, so expect a similar line there.
Use a real calendar
When the logic does what you want, install a provider and swap it in for the mock. Only the line that creates calendar changes; every call after it stays the same.
// npm install @calsdk/google
import { google } from '@calsdk/google';
const calendar = google({ accessToken: () => getFreshAccessToken() });Each provider page, linked from Choose a provider, says which token or password it needs and where it behaves differently.
Use a test calendar
Writes go to the real account. Point calendarId at a calendar made for testing until your code does what you expect.