LifterLMS Hooks and Filters: The Useful Ones, With Real Code

A practical reference to the LifterLMS hooks developers use most: enrollment, completion, quiz, order and registration actions, plus the filters that control access, lesson completion, redirects and the student dashboard, each with working code.

  • Add as a preferred source on Google
Illustration for “LifterLMS Hooks and Filters: The Useful Ones, With Real Code”

LifterLMS has hundreds of hooks. You will use about twenty of them for almost everything: reacting when a student enrolls or completes a course, changing who can access what, redirecting after an action, and rearranging course and lesson pages. This guide covers those LifterLMS hooks with code you can drop into a custom plugin.

It is written for WordPress developers who know how add_action() and add_filter() work. If you need a refresher, the WordPress hooks handbook is the place to start. Every hook name, argument and priority below was checked against the LifterLMS 10.3 source.

Put this code in a small custom plugin, not in a theme's functions.php. Business logic should not disappear when someone changes the theme. Prefix your functions, as we do here with llmsx_.

How LifterLMS hooks are named

You will see two prefixes. Older hooks start with lifterlms_ and newer ones with llms_. Both are current; the prefix tells you nothing about whether a hook is deprecated.

Several important hooks are dynamic, built from a variable. The completion hook, for example, is lifterlms_{$object_type}_completed, which gives you lifterlms_lesson_completed, lifterlms_section_completed and lifterlms_course_completed. Searching the plugin for the literal name will not find these, so search for the pattern or use the tables below.

Learning event actions

HookFires whenArguments
llms_user_enrolled_in_courseA student is enrolled in a course, by any route$user_id, $course_id
llms_user_added_to_membership_levelA student is added to a membership$user_id, $membership_id
llms_user_removed_from_courseA student is unenrolled or their access expires$user_id, $course_id, $trigger, $new_status
llms_user_removed_from_membershipThe same for memberships$user_id, $membership_id, $trigger, $new_status
lifterlms_lesson_completedA lesson is marked complete$user_id, $lesson_id
lifterlms_section_completedEvery lesson in a section is complete$user_id, $section_id
lifterlms_course_completedEvery lesson in a course is complete$user_id, $course_id
lifterlms_quiz_completedA quiz attempt is finished, pass or fail$user_id, $quiz_id, $attempt
lifterlms_quiz_passed / lifterlms_quiz_failedA finished attempt passed or failed$user_id, $quiz_id, $attempt

The enrollment hooks fire however enrollment happens: checkout, a free enroll button, an admin, the REST API or an add-on. That makes them the right place for integrations, not the checkout. $attempt is an LLMS_Quiz_Attempt object.

Example: send new enrollments to another system

<?php
add_action( 'llms_user_enrolled_in_course', 'llmsx_notify_crm_of_enrollment', 10, 2 );

function llmsx_notify_crm_of_enrollment( $user_id, $course_id ) {
	$user = get_userdata( $user_id );
	if ( ! $user ) {
		return;
	}

	wp_remote_post(
		'https://crm.example.com/hooks/enrollment',
		array(
			'timeout'  => 5,
			'blocking' => false,
			'body'     => array(
				'email'  => $user->user_email,
				'course' => get_the_title( $course_id ),
			),
		)
	);
}

The request is non-blocking so a slow CRM cannot slow down checkout. For anything where a lost message matters, queue the work instead, or use LifterLMS's outgoing webhooks, described in our REST API and webhooks introduction.

Example: alert the instructor when a student fails a quiz

<?php
add_action( 'lifterlms_quiz_failed', 'llmsx_quiz_failed_alert', 10, 3 );

function llmsx_quiz_failed_alert( $user_id, $quiz_id, $attempt ) {
	$quiz = llms_get_post( $quiz_id );
	if ( ! $quiz ) {
		return;
	}
	$author  = get_userdata( get_post_field( 'post_author', $quiz_id ) );
	$student = get_userdata( $user_id );
	if ( ! $author || ! $student ) {
		return;
	}

	wp_mail(
		$author->user_email,
		sprintf( 'Quiz failed: %s', $quiz->get( 'title' ) ),
		sprintf(
			'%1$s scored %2$s%% on attempt %3$d.',
			$student->display_name,
			round( (float) $attempt->get( 'grade' ), 2 ),
			(int) $attempt->get( 'attempt' )
		)
	);
}

Before writing code like this, check whether a built-in engagement already does the job. Many "send an email when" requests are covered by LifterLMS engagements without code.

Example: mark course completion for another system

A common request is to record completions somewhere other plugins or reports can read, such as user meta used by an HR export. The completion hook gives you exactly what you need:

<?php
add_action( 'lifterlms_course_completed', 'llmsx_record_completion', 10, 2 );

function llmsx_record_completion( $user_id, $course_id ) {
	$completed = get_user_meta( $user_id, 'llmsx_completed_courses', true );
	$completed = is_array( $completed ) ? $completed : array();

	$completed[ $course_id ] = current_time( 'mysql' );

	update_user_meta( $user_id, 'llmsx_completed_courses', $completed );
}

LifterLMS already stores completion dates itself, and its reporting screens and REST API read them. Copy them elsewhere only when another system genuinely needs its own copy, and remember that a student who marks a lesson incomplete again can make the course incomplete, which fires lifterlms_course_incompleted.

Order, payment and account actions

HookFires whenArguments
lifterlms_access_plan_purchasedAn order becomes completed or active, after enrollment$user_id, $plan_id
lifterlms_product_purchasedThe same, with the course or membership$user_id, $product_id
lifterlms_order_status_{status}An order changes to a status$order, $old_status, $new_status
lifterlms_transaction_status_{status}A transaction changes status, such as succeeded or failed$transaction, $old_status, $new_status
lifterlms_user_registeredA user registers through a LifterLMS form$user_id, $posted_data, $location

Order statuses in the dynamic hook have the llms- prefix removed, so you get lifterlms_order_status_completed, _active, _on-hold, _pending-cancel, _cancelled, _expired, _refunded and _failed. For registrations, $location is registration or checkout.

Example: flag a customer whose recurring order goes on hold

<?php
add_action( 'lifterlms_order_status_on-hold', 'llmsx_flag_on_hold_order', 10, 3 );

function llmsx_flag_on_hold_order( $order, $old_status, $new_status ) {
	update_user_meta( $order->get( 'user_id' ), 'llmsx_payment_problem', current_time( 'mysql' ) );
	$order->add_note( 'Customer flagged for payment follow-up.' );
}

Filters that change behaviour

FilterControls
llms_page_restrictedWhether the current user can see a post, and why not
llms_allow_lesson_completionWhether a lesson may be marked complete
llms_show_mark_complete_buttonWhether the Mark Complete button is shown
llms_lesson_complete_redirectWhere a student goes after completing a lesson
lifterlms_registration_redirectWhere a new user goes after registering
lifterlms_completed_transaction_redirectWhere a buyer goes after checkout
llms_get_student_dashboard_tabsThe student dashboard navigation
lifterlms_loop_columnsColumns in course and membership catalogs
llms_certificate_merge_dataMerge code values on awarded certificates
llms_get_post_contentThe full output of course, lesson, membership and quiz content

Example: require an external condition before completing a lesson

Say lesson 123 can only be completed after the student has submitted a form handled by another plugin that stores user meta:

<?php
add_filter( 'llms_allow_lesson_completion', 'llmsx_gate_lesson_completion', 10, 5 );

function llmsx_gate_lesson_completion( $allow, $user_id, $lesson_id, $trigger, $args ) {
	if ( 123 === (int) $lesson_id && ! get_user_meta( $user_id, 'llmsx_form_submitted', true ) ) {
		return false;
	}
	return $allow;
}

add_filter( 'llms_show_mark_complete_button', 'llmsx_hide_button_until_form', 10, 2 );

function llmsx_hide_button_until_form( $show, $lesson ) {
	if ( 123 === (int) $lesson->get( 'id' ) && ! get_user_meta( get_current_user_id(), 'llmsx_form_submitted', true ) ) {
		return false;
	}
	return $show;
}

Use both filters together. Hiding the button alone is cosmetic; blocking completion is what enforces the rule.

<?php
add_filter( 'llms_get_student_dashboard_tabs', 'llmsx_dashboard_support_link' );

function llmsx_dashboard_support_link( $tabs ) {
	$tabs['llmsx-support'] = array(
		'endpoint' => false,
		'nav_item' => true,
		'title'    => __( 'Get Help', 'llmsx' ),
		'url'      => home_url( '/support/' ),
	);
	return $tabs;
}

A tab with its own content and endpoint is also possible, but needs rewrite rules flushed and a content callback. A link like this needs neither.

Template hooks on course and lesson pages

Course and lesson pages are built from template functions attached to hooks. Remove one with remove_action() using the same priority, or add your own output at a priority between them.

HookFunctionPriority
lifterlms_single_course_before_summarylifterlms_template_single_video20
lifterlms_single_course_after_summarylifterlms_template_course_author40
lifterlms_single_course_after_summarylifterlms_template_single_prerequisites55
lifterlms_single_course_after_summarylifterlms_template_pricing_table60
lifterlms_single_course_after_summarylifterlms_template_single_course_progress60
lifterlms_single_course_after_summarylifterlms_template_single_syllabus90
lifterlms_single_course_after_summarylifterlms_template_single_reviews100
lifterlms_single_lesson_after_summarylifterlms_template_complete_lesson_link10
lifterlms_single_lesson_after_summarylifterlms_template_lesson_navigation20

One catch: courses edited in the block editor, Elementor or Beaver Builder are "migrated", and LifterLMS removes several of these actions because blocks, widgets or modules output the same elements instead. If remove_action() seems to do nothing, the element is probably coming from a block. Template changes beyond moving elements are covered in LifterLMS template overrides.

Debugging a hook that does not seem to fire

When a callback never runs, work through it in this order:

  1. Is the code loaded? A custom plugin must be active, and code in a theme only loads with that theme. Add a temporary error_log() at the top of the file.
  2. Did you register it in time? Hooks that fire during early requests, such as REST calls or background jobs, need your add_action() to run when your plugin loads, not inside a later hook like template_redirect.
  3. Is the argument count right? If you use more than one argument, the fourth parameter of add_action() must say so, as in the examples above.
  4. Did the event actually happen? Enrolling a student who is already enrolled does nothing, so llms_user_enrolled_in_course does not fire. Test with a fresh student.
  5. Is it running in the background? Some work happens in scheduled actions. Check LifterLMS > Status > Scheduled Actions to see whether jobs are queued, running or failing.

With WP_DEBUG_LOG on, a single error_log( print_r( func_get_args(), true ) ); inside your callback shows exactly what LifterLMS passes. Remove it before going live, because those arguments can include personal data.

Mistakes to avoid

  • Assuming a logged-in browser. Enrollment and completion hooks also fire from the REST API, WP-CLI, imports and scheduled tasks. Use the $user_id argument, never get_current_user_id(), inside action callbacks.
  • Redirecting inside an action. Calling wp_redirect() in an enrollment hook breaks checkout and API requests. Use the redirect filters.
  • Doing slow work inline. Remote calls and heavy queries in a completion hook slow down every Mark Complete click. Queue them with Action Scheduler, which LifterLMS already bundles.
  • Not handling repeats. A student can be enrolled, removed and enrolled again. Make callbacks safe to run twice.
  • Wrong priority on remove_action. It must match the priority used to add the action, as listed above.

Frequently asked questions

Which hook fires when a student completes a course?

lifterlms_course_completed, with the user ID and course ID. It fires once all lessons in the course are complete.

Is there a hook for when a student buys a course?

Use lifterlms_access_plan_purchased or lifterlms_product_purchased, which fire when an order becomes completed or active, or llms_user_enrolled_in_course if you care about access rather than payment.

Where do I find hooks not listed here?

Search the plugin source for do_action( and apply_filters(. Each one has a docblock describing its arguments.

Can I use these hooks to build a custom engagement trigger?

Yes. That is exactly how custom triggers are built; see custom engagement triggers.

What to do next

Pick the one automation your team does by hand most often, find its hook in the tables above, and build it in a custom plugin on staging. Log what the callback receives the first few times it runs, then remove the logging.

If the logic is business-critical, touches payments, or needs to talk to other systems reliably, our LifterLMS plugin development service builds and maintains exactly this kind of code. Describe the workflow and we will tell you which hooks it needs.

Add LifterLMS Expert to your preferred sources

Add lifterlmsexpert as a preferred source on Google, or open this article in your AI assistant to use it as a source.

  • Add as a preferred source on Google
Free consultation

Prefer expert help over DIY?

Skip the trial and error. Get specialist LifterLMS help and ship faster.

Talk to a LifterLMS Expert Explore our services

Replies within one business day