A CalendarEvent is a booking. It always lives on exactly one
Calendar (calendar_fk) and has a start_time / end_time plus an
IANA timezone. Around that core, it carries:
| Field | What it models |
|---|---|
attendees (via EventAttendance) |
Internal users participating, with RSVP status. |
external_attendees (via EventExternalAttendance) |
Non-user attendees identified by email + name. |
resources (via ResourceAllocation) |
Resource calendars allocated to the event. |
bundle_calendar, bundle_primary_event, is_bundle_primary |
Bundle membership (see calendar-bundles.md). |
appointment_type, appointment_type_selections |
Appointment type booking metadata (see appointment-types.md). |
recurrence_rule, recurrence_id, parent_recurring_object, is_recurring_exception |
Recurrence (see recurrence.md). |
bulk_modification_parent |
If the event is a continuation produced by a bulk modification split. |
external_id |
Stable id from the upstream provider for synced events. |
EventAttendance is for users with an account in the system. The
RSVP status (accepted, declined, pending) and downstream
notification logic use it. Example: an internal cardiologist invited to
a tumour-board video call.ExternalAttendee / EventExternalAttendance is for
non-account participants identified by email — typically the patient
on a clinic appointment or a referring physician copied on a consult.Both attend the same event; they're separate models because the system knows much more about internal users (permissions, notification preferences, calendar ownerships) than it does about external ones.
ResourceAllocation.ResourceAllocation ties an event to one or more resource calendars
(see calendars.md). The allocation has its own
RSVP status (think: "the OR-2 calendar provisionally accepts" pending
confirmation by the OR scheduler).
Examples:
OR-3
and C-arm fluoroscopy unit. If the C-arm is double-booked, the
scheduler can decline its allocation and reroute.Note on bookings via
AppointmentType: when a slot of an appointment type is filled with a resource calendar, the per-slot picks are stored inCalendarEventAppointmentTypeSelectionrather thanResourceAllocation. The two models coexist:ResourceAllocationis the older "this event uses these resources" mechanism;CalendarEventAppointmentTypeSelectionis the "which calendars satisfied each slot of the booking template" record.
A CalendarEvent knows whether it was created through a higher-level
booking primitive:
bundle_calendar is non-null when the event was created via a
BUNDLE calendar. is_bundle_primary=True marks the canonical event
(the one synced to the external provider); other child calendars get a
representation event or a BlockedTime. See
calendar-bundles.md.appointment_type is non-null when the event was booked via a
AppointmentType. The companion CalendarEventAppointmentTypeSelection rows
record which calendar from each slot's pool was picked. See
appointment-types.md.Both fields are independent of recurrence — a recurring weekly tumour board can absolutely be an appointment-type event, with each occurrence inheriting the same selections.
CalendarService (in calendar_integration/services/calendar_service.py)
is the main entry point. It:
only_calendars_available_in_ranges.For appointment-type/bundled bookings, callers should use the higher-level
services (AppointmentTypeService.create_appointment_type_event,
CalendarService.create_bundle_calendar + _create_bundle_event)
rather than create_event directly — those services handle picking the
primary calendar, propagating to children, and writing the per-slot or
per-bundle metadata in one transaction.
updateCalendarEventThe public GraphQL API's updateCalendarEvent mutation updates a single-calendar
event's title, description, internal attendees, external attendees and client
identifiers. Every field besides eventId is UNSET-defaulted: omitting a field
leaves it exactly as stored. A caller that supplies only title does not touch
attendees or identifiers.
That "omitted vs. supplied" distinction goes all the way down: CalendarEventInputData
is itself tri-state on title, description, attendances and external_attendances
(None = leave untouched), so the resolver passes UNSET straight through as None
and CalendarEventService.update_event skips the corresponding write entirely — no
assignment, no attendee reconciliation, no attendee webhook. It deliberately does not
read the event's current values and re-send them: that older shape could revert a
concurrent update to a field this caller never named, and re-sending every existing
external attendee (id included) put them all on update_event's update-in-place branch,
emitting one CALENDAR_EVENT_ATTENDEE_UPDATED webhook per attendee on a title-only
update.
On create_event there is nothing to leave untouched, so None behaves as the empty
value ("" / []) — identical to the pre-tri-state behavior for every caller.
updateCalendarEvent does not own startTime, endTime, timezone or
rruleString. Those stay on rescheduleCalendarEvent — the two mutations are
deliberately non-overlapping, and updateCalendarEvent always re-passes the event's
current time/timezone/recurrence-rule fields unchanged.
Owner-scoped tokens may only update events on calendars their owner owns; a
cross-owner eventId returns the same "Event not found." error a genuinely missing
event would, so existence is never leaked to a caller outside that scope.
Identifier writes on this mutation share scheduleEvent's validation and reject (with
the whole update rolled back) on: an invalid system URL; a blank/whitespace-only or
over-255-character identifier; a (system, identifier) pair already claimed by
another record of the same type in the organization; or two pairs in one payload that
normalize to the same system. (Two of the six identifier domain errors — an
out-of-allowlist target and a cross-organization target — can never be reached from a
caller-supplied body on either mutation, since the target and organization are always
resolved server-side.)
RSVPStatus (accepted, declined, pending) is shared between
EventAttendance, EventExternalAttendance, and ResourceAllocation.
A few practical patterns:
EventExternalAttendance —
"patient confirmed the visit" updates this row.EventAttendance, often synced from the provider's reply on
Google/Outlook.