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.
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:
| Column | Type | Description |
|---|---|---|
joining_instructions | TEXT | Asset-level joining instructions (fallback when a session has none) |
booking_instructions | TEXT | Instructions 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:
| Column | Type | Description |
|---|---|---|
content_id | INT | FK to the event asset |
start_datetime | DATETIME | Start time, stored in UTC |
end_datetime | DATETIME | End time, stored in UTC |
timezone | VARCHAR(50) | IANA timezone for display (e.g. Europe/London) |
location | VARCHAR(255) | Physical address or virtual meeting name |
link_url | VARCHAR(2048) | Virtual meeting link (Zoom, Teams, etc.) |
capacity | INT | Max attendees (optional) |
facilitator | VARCHAR(255) | Name of the session facilitator |
joining_instructions | TEXT | Session-level instructions; overrides the asset-level value |
reference | VARCHAR | External reference ID (optional) |
is_deleted | BOOLEAN | Soft delete flag |
Validation rules:
end_datetimemust be afterstart_datetimetimezonemust be a valid IANA timezone string (validated viaIntl.DateTimeFormat)- Datetimes must be ISO 8601 UTC strings (
YYYY-MM-DDTHH:MM:SSZ) - A session cannot be deleted while it has users with
bookedorattendedstatus
Bookings — user_event_session table
Tracks each user's relationship to a session:
| Column | Type | Description |
|---|---|---|
event_session_id | INT | FK to event_session |
user_id | INT | FK to users |
content_id | INT | Denormalised FK to content (for query performance) |
status | ENUM | booked, cancelled, attended, noshow |
is_current | BOOLEAN | Whether 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 anattendedcurrent 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:
userAssets.statusis set tocompletedwithstatus_timeequal to the session'send_datetime.- An activity event is logged:
category: 'asset', action: 'read'. - An activity event is logged:
category: 'eventSession', action: 'attended'. - 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):
| Trigger | Email type | Notes |
|---|---|---|
User added with booked status | eventBookingConfirmation | Includes ICS attachment |
| Session fields updated (date, location, joining instructions) | eventBookingUpdate | Includes updated ICS attachment |
Status changed to cancelled, or user moved to a different session | eventBookingCancellation | ICS with METHOD:CANCEL |
ICS generation:
- UID is derived from
sessionId+ region for global uniqueness. - Confirmations and updates use
METHOD:REQUEST; cancellations useMETHOD:CANCEL. - The session-level
joining_instructionsis 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 linkUrlis 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
eventpriority pill slot rendersEventSessionPillwhen 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:
- SessionsListPage — paginated table of all sessions, sorted by date.
- SessionDetailsPage — create or edit a session (date/time, timezone, location, capacity, virtual link, facilitator, reference, joining/booking instructions).
- 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).
| Method | Path | Description |
|---|---|---|
GET | /api/assets/:assetId/sessions | List sessions (paginated, sorted by startDate) |
POST | /api/assets/:assetId/sessions | Create a session |
GET | /api/assets/:assetId/sessions/:sessionId | Get session detail |
PATCH | /api/assets/:assetId/sessions/:sessionId | Update session |
DELETE | /api/assets/:assetId/sessions/:sessionId | Soft-delete session (blocked if active attendees exist) |
GET | /api/assets/:assetId/sessions/:sessionId/attendees | List attendees (filterable by status and search) |
POST | /api/assets/:assetId/sessions/:sessionId/attendees | Add users with a status |
PATCH | /api/assets/:assetId/sessions/:sessionId/attendees | Update attendee status |
GET | /api/assets/:assetId/sessions/:sessionId/attendees/check-eligibility | Preflight: access + capacity + conflict check |
Key files
| Area | Path |
|---|---|
| Content type constant | api/constants/contentTypes.js |
| Sessions routes | api/routes/assets/{assetId}/sessions/ |
| Attendees utils (business logic) | api/routes/assets/{assetId}/sessions/{sessionId}/attendees/utils.js |
| DB model — session | api/db/models/eventSession.js |
| DB model — booking | api/db/models/userEventSession.js |
| Email — confirmation/update | api/utils/emails/sendEventBookingEmail.js |
| Email — cancellation | api/utils/emails/sendBookingCancellationEmails.js |
| Reports query | api/db/methods/reports/eventsList.js |
| TypeScript types | frontend/src/common/types/events.ts |
| Asset viewer (user) | frontend/src/common/screens/AssetViewerUI/Content/EventAssetContent.tsx |
| Admin sessions manager | frontend/src/common/screens/EventSessionsManager/ |
| Card controller | frontend/src/common/components/ContentCards/controllers/useCardController.tsx |
| Join link utility | frontend/src/common/utils/shouldShowJoinMeetingLink.ts |
| EventSessionPill | frontend/src/common/components/Pill/PillVariants/EventSessionPill/ |
| EventStatusPill | frontend/src/common/components/Pill/PillVariants/EventStatusPill/ |
| Schema migration | tools/db-migrate/migrations/20260325162438-add-events-and-sessions.js |