Skip to main content

Events 📅

Events are a content type that lets admins create bookable sessions with attendance tracking, ICS calendar attachments in booking emails, and completion integration. An event asset can have multiple sessions (e.g. the same training run on different dates), and users are booked onto individual sessions rather than the event itself.

Enabling events

Events require the eventAssets feature flag to be enabled in the domain settings. Without it, the event type is not available when creating assets.

Data model

Events span three database layers:

Event asset — content table

An event is a regular asset row with type = 'event'. Two event-specific columns live alongside the standard asset fields:

ColumnTypeDescription
joining_instructionsTEXTAsset-level joining instructions (fallback when a session has none)
booking_instructionsTEXTInstructions shown to users before they book

Events are excluded from AI ingestion by default (exclude_from_ai = true).

Sessions — event_session table

Each session is a schedulable occurrence of the event:

ColumnTypeDescription
content_idINTFK to the event asset
start_datetimeDATETIMEStart time, stored in UTC
end_datetimeDATETIMEEnd time, stored in UTC
timezoneVARCHAR(50)IANA timezone for display (e.g. Europe/London)
locationVARCHAR(255)Physical address or virtual meeting name
link_urlVARCHAR(2048)Virtual meeting link (Zoom, Teams, etc.)
capacityINTMax attendees (optional)
facilitatorVARCHAR(255)Name of the session facilitator
joining_instructionsTEXTSession-level instructions; overrides the asset-level value
referenceVARCHARExternal reference ID (optional)
is_deletedBOOLEANSoft delete flag

Validation rules:

  • end_datetime must be after start_datetime
  • timezone must be a valid IANA timezone string (validated via Intl.DateTimeFormat)
  • Datetimes must be ISO 8601 UTC strings (YYYY-MM-DDTHH:MM:SSZ)
  • A session cannot be deleted while it has users with booked or attended status

Bookings — user_event_session table

Tracks each user's relationship to a session:

ColumnTypeDescription
event_session_idINTFK to event_session
user_idINTFK to users
content_idINTDenormalised FK to content (for query performance)
statusENUMbooked, cancelled, attended, noshow
is_currentBOOLEANWhether this is the user's controlling session for this event

The is_current flag enforces that a user has at most one controlling session per event at a time. When a user is moved to a new session, the old session's is_current is cleared.


Attendance lifecycle

booked ──► attended
──► cancelled
──► noshow

Status transitions are made by admins via the attendees API. The rules for the controlling session (is_current) are:

  • A row becomes current when its status is set to booked.
  • If the user has no other current session, the new row becomes current regardless of status.
  • When status is set to attended, the new row becomes current unless the user already has an attended current session that started later.

Capacity and conflict checking

Before adding or updating a booking, the API exposes a preflight endpoint (GET .../attendees/check-eligibility) that returns:

  • 409 if the session is at capacity
  • A warning if the user is already booked on a different session of the same event (the old booking is auto-cancelled with an email on confirm)

Admins can bypass conflict warnings by passing overrideWarnings: true.


Completion tracking

When a user's attendance status is set to attended:

  1. userAssets.status is set to completed with status_time equal to the session's end_datetime.
  2. An activity event is logged: category: 'asset', action: 'read'.
  3. An activity event is logged: category: 'eventSession', action: 'attended'.
  4. Completion rates are recalculated asynchronously via recalculateCompletionForUsers().

When a status is revoked back to cancelled or noshow, the userAssets row is reset to notstarted if the user had zero opens, and completion rates are recalculated again.


Email notifications

Three email types are sent automatically (future sessions only):

TriggerEmail typeNotes
User added with booked statuseventBookingConfirmationIncludes ICS attachment
Session fields updated (date, location, joining instructions)eventBookingUpdateIncludes updated ICS attachment
Status changed to cancelled, or user moved to a different sessioneventBookingCancellationICS with METHOD:CANCEL

ICS generation:

  • UID is derived from sessionId + region for global uniqueness.
  • Confirmations and updates use METHOD:REQUEST; cancellations use METHOD:CANCEL.
  • The session-level joining_instructions is used in the ICS description; falls back to the asset-level value.
  • Organiser info comes from the domain configuration.

Relevant source: api/utils/emails/sendEventBookingEmail.js, api/utils/emails/sendBookingCancellationEmails.js.


Frontend components

Asset viewer

EventAssetContent (frontend/src/common/screens/AssetViewerUI/Content/EventAssetContent.tsx) renders the user-facing event page: title, description, session details (date/time, timezone, location), booking and joining instructions, and the join meeting button.

The join meeting button (MeetingLink) is shown only when:

  • The user's status is booked
  • The event is happening today (same calendar date)
  • The current time is before the session's end_datetime
  • linkUrl is populated

The link is routed through /api/url?url= for tracking.

Content card badges

EventSessionPill (frontend/src/common/components/Pill/PillVariants/EventSessionPill/) appears on content cards and shows the next session date as "Today", "Tomorrow", or a short date. It is only rendered while the session date is in the future (shouldShowEventPill()).

EventStatusPill (frontend/src/common/components/Pill/PillVariants/EventStatusPill/) shows the user's attendance status with colour coding:

  • Green: booked, attended
  • Red: cancelled, noshow

useCardController

In useCardController.tsx, events receive two pieces of special treatment:

  • The event priority pill slot renders EventSessionPill when the user has a future booking.
  • File processing status checks are skipped for events (contentItem.type !== 'event'), since events have no uploaded file.

Admin management

The EventSessionsManager (frontend/src/common/screens/EventSessionsManager/) provides three screens accessible from the asset editor:

  1. SessionsListPage — paginated table of all sessions, sorted by date.
  2. SessionDetailsPage — create or edit a session (date/time, timezone, location, capacity, virtual link, facilitator, reference, joining/booking instructions).
  3. SessionAttendeesPage — add users to a session, change their status, remove them; shows capacity warnings; supports bulk operations.

The modal wrapper is at frontend/src/common/screens/EventSessionsManagerModal/.

Admins can also quick-create an event from the asset library via NewEvent (frontend/src/admin/pages/assets/components/AddContentWidget/NewEvent.tsx).


Reports

Two event-specific reports are available in the admin reports section (added in migration 20260409064235):

  • Events list — All sessions in a date range with per-session attendance counts (booked, attended, no-show).
  • Events — Per-user participation details across all event sessions.

Report logic: api/db/methods/reports/eventsList.js.


API endpoints

All endpoints require the assetAdminAndEventsEnabled permission (admin role + eventAssets flag).

MethodPathDescription
GET/api/assets/:assetId/sessionsList sessions (paginated, sorted by startDate)
POST/api/assets/:assetId/sessionsCreate a session
GET/api/assets/:assetId/sessions/:sessionIdGet session detail
PATCH/api/assets/:assetId/sessions/:sessionIdUpdate session
DELETE/api/assets/:assetId/sessions/:sessionIdSoft-delete session (blocked if active attendees exist)
GET/api/assets/:assetId/sessions/:sessionId/attendeesList attendees (filterable by status and search)
POST/api/assets/:assetId/sessions/:sessionId/attendeesAdd users with a status
PATCH/api/assets/:assetId/sessions/:sessionId/attendeesUpdate attendee status
GET/api/assets/:assetId/sessions/:sessionId/attendees/check-eligibilityPreflight: access + capacity + conflict check

Key files

AreaPath
Content type constantapi/constants/contentTypes.js
Sessions routesapi/routes/assets/{assetId}/sessions/
Attendees utils (business logic)api/routes/assets/{assetId}/sessions/{sessionId}/attendees/utils.js
DB model — sessionapi/db/models/eventSession.js
DB model — bookingapi/db/models/userEventSession.js
Email — confirmation/updateapi/utils/emails/sendEventBookingEmail.js
Email — cancellationapi/utils/emails/sendBookingCancellationEmails.js
Reports queryapi/db/methods/reports/eventsList.js
TypeScript typesfrontend/src/common/types/events.ts
Asset viewer (user)frontend/src/common/screens/AssetViewerUI/Content/EventAssetContent.tsx
Admin sessions managerfrontend/src/common/screens/EventSessionsManager/
Card controllerfrontend/src/common/components/ContentCards/controllers/useCardController.tsx
Join link utilityfrontend/src/common/utils/shouldShowJoinMeetingLink.ts
EventSessionPillfrontend/src/common/components/Pill/PillVariants/EventSessionPill/
EventStatusPillfrontend/src/common/components/Pill/PillVariants/EventStatusPill/
Schema migrationtools/db-migrate/migrations/20260325162438-add-events-and-sessions.js