Skip to main content

Hooks and events

Every hook and event a plugin can subscribe to, when each one is delivered, and the keys in its payload.

Subscribing​

A plugin subscribes in its hooks.py. When the plugin loads, CMDX calls register_hooks with the backend's hook manager:

from core.plugin_api import HookContext, HookManager


async def on_course_completed(ctx: HookContext) -> None:
user_id = ctx.data.get("user_id")
...


def register_hooks(hook_manager: HookManager) -> None:
hook_manager.register("course.completed", on_course_completed, priority=50)
  • The first argument is the hook or event name from the tables below.
  • Handlers for the same name run one after another, lowest priority first. The default is 50.
  • A handler receives a HookContext. ctx.hook_point is the name, ctx.data is the payload, a dictionary, and ctx.tenant_id is the tenant the event happened in.
  • Handlers can be async or plain functions. Plain functions run in a worker thread so they don't block the server.
  • The @hook decorator and register_decorated_hooks in core.plugin_api.hooks do the same with a decorator.

By default a handler can't stop the operation that fired it. If it raises an exception, CMDX logs it and the remaining handlers still run.

To veto an operation, register the handler with failure_mode="abort". Its exception then skips the remaining handlers and is raised again. It fails the operation only for a hook delivered during it, because only then is the caller waiting.

A handler can also set ctx.aborted = True, with an optional ctx.abort_reason. CMDX skips the remaining handlers, but the operation itself carries on.

When handlers run​

Each name is delivered in one of three ways. The Delivery column in the tables says which.

DeliveryWhen the handler runsDoes the caller wait?Can it see a change that is later rolled back?
DuringInside the operation, before its changes are savedYesYes
After saveOnce the request's database changes are savedNoNo
BackgroundSoon after the event is emitted, in a background task. For an event emitted by a worker, once the worker's task has succeededNoYes for an event emitted during a request; no for one emitted by a worker task

For handlers that run after save or in the background, the request's database session is already closed. A handler that needs the database opens its own session. Keep handlers short, and hand slow work to a background task.

Ids and times in payloads are usually strings. Keys in square brackets are sent only in some cases. Not every payload carries a tenant id. A tenant_id or organization_id key with no tenant is None.

ctx.tenant_id is a UUID: the payload's tenant_id or organization_id, otherwise the tenant of the request that emitted the event, otherwise None. A plugin whose manifest sets multitenant="per_tenant", the default, receives an event only when ctx.tenant_id is a tenant that has the plugin enabled. An event with no tenant reaches only plugins with multitenant="global".

note

Some hook points are defined in core/hooks/points.py but nothing fires them yet, and they are left out of this page. This includes the model.* points: whether they are kept as reserved or removed is still being decided.

Events emitted by Celery workers and scheduled jobs reach plugin hooks too. A worker sets up the same hooks as the API, including those of enabled plugins, when it runs its first task. It holds the events a task emits and delivers them once the task has succeeded, so a task that fails delivers none. A worker loads plugins once per process, so restart the Celery workers after you enable, disable or reload a plugin.

Hook points​

These names are members of HookPoint in core.hooks.points.

Requests​

NameFired whenDeliveryPayload keys
request.startedThe API receives a requestDuringpath, method
request.completedThe API has a responseDuringpath, method, status_code
request.failedHandling the request raised an errorDuringpath, method, error

Users and sign-in​

NameFired whenDeliveryPayload keys
user.registeredAn account is createdAfter saveuser_id, email, [send_email, username, handle, first_name, middle_name, last_name, gender, phone_number, locale]
user.loginA user signs in with a passwordAfter saveuser_id, ip, user_agent, [verified_tenant_id]
user_invitedSomeone is invited to CMDXAfter saveemail, action_url, role, subject, expires_in, tenant_id
password_reset_requestedA password reset is requestedAfter saveuser_id, email, action_url, subject, expires_in
password_changedA password is changedAfter saveuser_id, subject, [set_by_admin]
email_verification_requestedAn email verification link is sentAfter saveuser_id, email, action_url, subject, expires_in
email_verifiedA user verifies their email addressAfter saveuser_id
account_suspendedAn administrator suspends an accountAfter saveuser_id, subject
role_assignment_createdA role is assigned to a userAfter saveuser_id, user_name, role_name
role_assignment_revokedA role is taken away from a userAfter saveuser_id, user_name, role_name

Courses and enrollment​

NameFired whenDeliveryPayload keys
course.createdA course is created or clonedBackgroundcourse_id, created_by, tenant_id, [cloned_from]
course.enrolledA learner is enrolled in a courseDuring or backgrounduser_id, student_id, course_id, learning_module_id, course_name, context_type, context_id, tenant_id, [user_name, student_name, locale, instructor_id]
course.completedA learner completes a courseBackgrounduser_id, course_id, tenant_id
user.enrolledAn enrollment is created or revokedDuringenrollment_id, user_id, learning_module_id, tenant_id, method, status, event (enrollment.created or enrollment.revoked)

In course.enrolled, course_id is the course's own id and learning_module_id is the learning module the learner was enrolled in.

Groups​

NameFired whenDeliveryPayload keys
group.createdA group is createdDuringevent, group_id, actor_id, tenant_id, emitted_at
group.member_addedA user joins a groupDuringevent, group_id, user_id, actor_id, tenant_id, emitted_at
group.member_removedA user leaves a groupDuringevent, group_id, user_id, actor_id, tenant_id, emitted_at

group.created runs once the group is saved, although the caller still waits for it.

Assignments​

NameFired whenDeliveryPayload keys
assignment.submittedA learner submits an assignmentAfter saveassignment_id, submission_id, user_id, learner_id, tenant_id
assignment.gradedA submission is gradedAfter saveassignment_id, submission_id, grader_id, user_id, learner_id, tenant_id, score, passed

URL resources​

NameFired whenDeliveryPayload keys
url_resource.createdA URL resource is created, including by cloningAfter savetenant_id, organization_id, url_resource_id, status, url, [learning_item_id, course_id, user_id]
url_resource.updatedA URL resource is changedAfter saveSame as url_resource.created
url_resource.clonedA URL resource is clonedAfter saveSame as url_resource.created, plus source_url_resource_id
url_resource.publishedA URL resource is publishedAfter saveSame as url_resource.created
url_resource.archivedA URL resource is archivedAfter saveSame as url_resource.created
url_resource.deletedA URL resource is deletedAfter saveSame as url_resource.created
url_resource.visitedA learner opens a URL resourceAfter savetenant_id, url_resource_id, user_id, visit_id, course_id, learning_item_id, status, url, visited_at

HTML pages​

NameFired whenDeliveryPayload keys
html_page.publishedAn HTML page is publishedAfter savepage_id, tenant_id, organization_id, course_id, user_id, version_number, published_at
html_page.archivedAn HTML page is archivedAfter savepage_id, tenant_id, organization_id, course_id, user_id, archived_at
html_page.viewedA learner views an HTML pageAfter savepage_id, tenant_id, organization_id, course_id, user_id, completion_id, view_count, viewed_at, completed_at, last_viewed_at, is_first_view

Trainings​

NameFired whenDeliveryPayload keys
training.publishedA training is publishedDuringtraining_id, course_id, tenant_id, published_by
session.startedA training session startsDuringsession_id, training_id, session_type, tenant_id, started_by
session.endedA training session endsDuringsession_id, training_id, tenant_id, ended_by

Feedback​

All are delivered after save.

NameFired whenPayload keys
feedback.form.createdA feedback form is createdform_id, course_id, tenant_id, created_by
feedback.form.updatedA feedback form is changedform_id, course_id, tenant_id, updated_by
feedback.form.deletedA feedback form is deletedform_id, tenant_id, deleted_by
feedback.form.publishedA feedback form is publishedform_id, course_id, tenant_id, published_by
feedback.form.closedA feedback form stops taking responsesform_id, tenant_id, response_count
feedback.question.createdA question is addedform_id, question_id, question_type, option_count, tenant_id, created_by
feedback.question.updatedA question is changedform_id, question_id, question_type, options_synced, option_count, tenant_id, updated_by
feedback.question.deletedA question is deletedform_id, question_id, tenant_id, deleted_by
feedback.question.reorderedQuestions are reorderedform_id, question_count, tenant_id, reordered_by
feedback.submittedA learner submits a responseform_id, response_id, user_id, course_id, tenant_id
feedback.response.deletedA response is deletedform_id, response_id, tenant_id, deleted_by

Events​

These names are emitted on the event dispatcher in core.events and delivered to hook handlers in the background. Subscribe to them with hook_manager.register exactly like a hook point.

Courses​

NameFired whenPayload keys
course.updatedA course is changedcourse_id, tenant_id
course.deletedA course is deletedcourse_id, tenant_id
course.publishedA course is publishedcourse_id, tenant_id
course.archivedA course is archivedcourse_id, tenant_id
course.owner_transferredA course gets a new ownercourse_id, previous_owner_id, new_owner_id, actor_id

Enrollments​

NameFired whenPayload keys
enrollment.createdAn enrollment is createdenrollment_id, user_id, learning_module_id, method, status, actor_id, tenant_id
enrollment.revokedAn enrollment is revokedenrollment_id, user_id, learning_module_id, method, status, revoke_reason, actor_id, tenant_id, [course_name, user_name, locale]
enrollment.completedAn enrollment is completedenrollment_id, user_id, learning_module_id, method, status, actor_id, tenant_id
enrollment.expiredAn enrollment expiresenrollment_id, user_id, learning_module_id, status, actor_id, tenant_id, [course_name, user_name, locale]
enrollment.reminderAn enrollment reminder is dueSame as enrollment.expired
enrollment.droppedA course is force-deleted and its active or pending enrollments are droppedenrollment_id, user_id, learning_module_id, method, status, reason, course_id, actor_id, tenant_id
enrollment.progress_updatedA learner's progress in an enrollment changestenant_id, user_id, learning_item_id, status

Enrollment keys and rules​

NameFired whenPayload keys
key.generatedAn enrollment key is generatedkey_id, learning_module_id, actor_id, tenant_id
key.batch_generatedA batch of keys is generatedbatch_id, count, learning_module_id, actor_id, tenant_id
key.redeemedA learner redeems a keykey_id, user_id, enrollment_id, actor_id, tenant_id
key.revokedA key is revokedkey_id, status_before, status_after, actor_id, tenant_id
key.expiredA key expireskey_id, actor_id, tenant_id
rule.createdAn enrollment rule is createdrule_id, rule_type, tenant_id, actor_id
rule.updatedAn enrollment rule is changedrule_id, rule_type, tenant_id, actor_id
rule.deletedAn enrollment rule is deletedrule_id, rule_type, tenant_id, actor_id
rule.runAn enrollment rule runsrule_id, tenant_id, actor_id, rule_type

Learning items, attempts and grades​

These events share one payload: tenant_id, entity_id, user_id, timestamp, event_type and payload. The table lists the keys inside payload.

NameFired whenKeys inside payload
learning_item.createdA learning item is createdkind, title
learning_item.updatedA learning item is changedkind, title, [status]
learning_item.publishedA learning item is publishedkind, title, status
learning_item.archivedA learning item is archivedkind, title, status
attempt.submittedA learner submits a quiz attemptkind, quiz_id, learning_item_id, attempt_id, learner_id, attempt_number, score_pct, max_score, passed, status
attempt.returnedA quiz attempt is returned to the learnerSame as attempt.submitted, plus return_reason
submission.gradedA quiz attempt or assignment submission is gradedQuiz: same as attempt.submitted. Assignment: learning_item_id, assignment_id, submission_id
submission.regradedA graded submission is graded againSame as submission.graded
progress.recordedA learner's progress on a learning item is recordedstatus, score
completion.achievedA learner completes a learning itemkind, or manual when an instructor marks it
grade.overrideA grade is overriddenoverride_score, overridden_by, grade_item_id
video.uploadedA video is uploadedlearning_item_id, file_id, source_mode
video.viewedA learner watches a videolearning_item_id, max_playhead_seconds, watch_time_seconds
video.completedA learner finishes a videolearning_item_id, completion_pct, max_playhead_seconds, duration_seconds

These events use a flat payload:

NameFired whenPayload keys
learning_item.availableA learning item becomes available to a learnertenant_id, learning_item_id
learning_item.unavailableA learning item stops being available to a learnertenant_id, learning_item_id
learning_item.completedA learner completes a learning item in an enrollmentuser_id, learning_module_id, progress_pct, tenant_id
gradebook.recomputeA learner's grades need recalculatingtenant_id, user_id, learning_item_id
certificate.check_eligibilityA learner may have become eligible for a certificatetenant_id, user_id, learning_item_id
module.event_handler_failedA learning item event handler failedevent_type, entity_id, tenant_id, handler

Roles and permissions​

NameFired whenPayload keys
role_createdA role is createdrole_id, role_data
role_updatedA role is changedrole_id, changes
role_deactivatedA role is deactivatedrole_id
role_capabilities_addedCapabilities are added to a rolerole_id, capability_ids
role_capabilities_removedCapabilities are removed from a rolerole_id, capability_ids
role_capabilities_replacedA role's capabilities are replacedrole_id, capability_keys
context_createdA permission context is createdcontext_id, context_data
context_overrides_replacedA context's permission overrides are replacedcontext_id
role_switch_startedA user switches to another roleactor_id, role_id
role_switch_endedA user switches backactor_id, session_id
role_switch_blockedA role switch is refusedactor_id, role_id
rbac_bundle_exportedRoles and permissions are exportedactor_id
rbac_bundle_importedRoles and permissions are importedactor_id, dry_run
rbac_bundle_import_dry_runAn import is checked without applying itactor_id, dry_run

Profiles, dashboards and preferences​

NameFired whenPayload keys
user.profile.createdA user profile is createduser_id, profile_data
user.profile.updatedA user profile is changeduser_id, changes
user.profile.deletedA user profile is deleteduser_id
user.profile_field_category.createdA profile field category is createdcategory_id, tenant_id, category_data
user.profile_field_category.updatedA profile field category is changedcategory_id, tenant_id, changes
user.profile_field_category.movedA profile field category is movedcategory_id, tenant_id, new_parent_id
user.profile_field_category.deletedA profile field category is deletedcategory_id, tenant_id
user.profile_field.createdA custom profile field is createdprofile_field_id, tenant_id, profile_field_data
user.profile_field.deletedA custom profile field is deletedprofile_field_id, tenant_id
user.profile_field.reactivatedA deleted profile field is restoredprofile_field_id, tenant_id
user.dashboard.createdA dashboard is createddashboard_id, user_id, dashboard_data
user.dashboard.updatedA dashboard is changeddashboard_id, changes
user.dashboard.deletedA dashboard is deleteddashboard_id, user_id
user.widget.createdA dashboard widget is addedwidget_id, dashboard_id, widget_data
user.widget.updatedA dashboard widget is changedwidget_id, changes
user.widget.deletedA dashboard widget is removedwidget_id, dashboard_id
user.import.completedA bulk user import finishesjob_id, total_rows, processed_rows
user.preferences.createdA user's preferences are first saveduser_id, [fields]
user.preferences.updatedA user's preferences are changeduser_id, [fields]
user.preferences.resetA user's preferences are resetuser_id

Notifications​

NameFired whenPayload keys
notification.delivery_failedA notification can't be delivered on a channel after its retries run outdelivery_id, notification_id, channel, user_id, error_message, retry_count

Next​