Adaptivity

Adaptive Learning Paths

⏱ 16 min

“Adaptive” gets used to describe two very different things: software that quietly reroutes a student without telling them, and software that notices a struggle and suggests a next step a student or instructor can accept or ignore. This lesson builds the second kind. It tracks real LearnDash progress signals and turns them into a plain recommendation, never a forced redirect or an automatically relocked lesson.

What you'll learn in this lesson
Capturing progress signals from real LearnDash hooks
learndash_quiz_completed and learndash_lesson_completed, not a guess at what LearnDash tracks internally.
Why you should log a hook's payload before trusting its shape
These two hooks aren't stabilized in the public reference to the same degree as their function signatures.
Building a next-step recommendation with generate_text()
Combining recent quiz performance with the remaining course steps.
Keeping the recommendation a suggestion, not an enforcement
Storing it as visible advice, never changing what content a student can access.
Prerequisites

LearnDash installed and active, with at least one course that has a graded quiz. Course 5’s Generating Text With wp_ai_client_prompt().

Step 1: Capture progress signals from real hooks

learndash_quiz_completed fires an action with $quizdata (array) and $user (WP_User) after a quiz is marked complete. learndash_lesson_completed fires with $lesson_data (array) after a lesson is marked complete. Both are real, documented action hooks.

Confirm the exact array keys on your install first

LearnDash’s public hook reference documents that these two hooks fire with an array of completion data, but it doesn’t stabilize the exact array keys the way it stabilizes function signatures like learndash_get_course_id(). In current LearnDash releases, $quizdata commonly includes keys like course, pass, and percentage, but treat that as something to confirm on your own installed version, not assume blindly. Log it once before writing anything that depends on a specific key:

add_action( 'learndash_quiz_completed', function ( $quizdata, $user ) {
	error_log( print_r( $quizdata, true ) );
}, 10, 2 );

Once confirmed, record the signal:

// File: lms-tutor/adaptive-paths.php
add_action( 'learndash_quiz_completed', function ( $quizdata, $user ) {
	$course_id = isset( $quizdata['course'] ) ? (int) $quizdata['course'] : 0;

	if ( ! $course_id ) {
		return;
	}

	update_user_meta( $user->ID, "_lms_tutor_last_quiz_{$course_id}", array(
		'percentage' => $quizdata['percentage'] ?? null,
		'pass'       => $quizdata['pass'] ?? null,
		'time'       => time(),
	) );
}, 10, 2 );

Step 2: Gather progress and remaining steps for a recommendation

// File: lms-tutor/adaptive-paths.php
function lms_tutor_gather_progress( int $user_id, int $course_id ): array {
	$last_quiz = get_user_meta( $user_id, "_lms_tutor_last_quiz_{$course_id}", true );
	$steps     = learndash_get_course_steps( $course_id );

	$completed = array();
	foreach ( $steps as $step_id ) {
		if ( learndash_is_lesson_complete( $user_id, $step_id, $course_id ) ) {
			$completed[] = $step_id;
		}
	}

	return array(
		'last_quiz'  => $last_quiz ?: null,
		'remaining'  => array_values( array_diff( $steps, $completed ) ),
	);
}

learndash_get_course_steps( $course_id ) returns every lesson and topic in the course, which combined with a completion check gives a real, current picture of what’s left, not a guess based on enrollment date alone.

Step 3: Ask for a recommendation, in a fixed, parseable format

// File: lms-tutor/adaptive-paths.php
function lms_tutor_recommend_next_step( int $user_id, int $course_id ): array {
	$progress = lms_tutor_gather_progress( $user_id, $course_id );

	if ( empty( $progress['remaining'] ) ) {
		return array( 'recommendation' => 'course_complete', 'reason' => 'All steps are complete.' );
	}

	$next_step_id  = $progress['remaining'][0];
	$next_title    = get_the_title( $next_step_id );
	$last_score    = $progress['last_quiz']['percentage'] ?? null;
	$last_pass     = $progress['last_quiz']['pass'] ?? null;

	$prompt = <<<PROMPT
A student's most recent quiz result in this course was: percentage {$last_score}, passed: {$last_pass}.
The next unfinished step in the course is: "{$next_title}".
Recommend exactly one of "continue" or "review", with a one-sentence reason. If the
most recent quiz score suggests the student struggled, recommend reviewing the current
material again before moving on. Respond in exactly this format:

Recommendation: <continue or review>
Reason: <one sentence>
PROMPT;

	$response = wp_ai_client_prompt( $prompt )->generate_text();

	$recommendation = 'continue';
	$reason         = '';

	if ( preg_match( '/Recommendation:\s*(\w+)/i', $response, $m ) ) {
		$recommendation = strtolower( trim( $m[1] ) );
	}
	if ( preg_match( '/Reason:\s*(.+)/i', $response, $m ) ) {
		$reason = trim( $m[1] );
	}

	if ( ! in_array( $recommendation, array( 'continue', 'review' ), true ) ) {
		$recommendation = 'continue'; // Unparseable output defaults to no change in path.
	}

	return array( 'recommendation' => $recommendation, 'next_step_id' => $next_step_id, 'reason' => $reason );
}

An unparseable response defaults to continue, the option that changes nothing about the student’s normal path forward, rather than defaulting to review, which would needlessly insert extra work into a student’s course based on a response the code couldn’t actually confirm.

Step 4: Surface it as a suggestion, never an enforcement

Store the result and show it as a dismissible note in the student’s dashboard, or an instructor’s view of that student’s progress:

// File: lms-tutor/adaptive-paths.php
$rec = lms_tutor_recommend_next_step( get_current_user_id(), $course_id );
update_user_meta( get_current_user_id(), "_lms_tutor_recommendation_{$course_id}", $rec );

Nothing in this lesson calls learndash_process_mark_complete(), changes lesson order, or locks any step. The recommendation is data a student or instructor can act on, or ignore entirely and keep moving through the course exactly as they would have anyway.

Test it: simulate a low quiz score and check the recommendation

wp-env run cli wp eval '
update_user_meta( 5, "_lms_tutor_last_quiz_3", array( "percentage" => 40, "pass" => false, "time" => time() ) );
print_r( lms_tutor_recommend_next_step( 5, 3 ) );
'

Confirm the recommendation leans toward review with a reason mentioning the low score, then repeat with a high, passing score and confirm it leans toward continue.

Recommending review off a single bad quiz attempt

One low score can be an off day, not a real gap in understanding. Recommending review after every single below-threshold attempt will nag students unnecessarily and erode trust in the recommendation entirely. Consider requiring two consecutive weak attempts, or averaging the last two or three scores, before recommending review, and always leave it as a suggestion the student can dismiss.

Recap

Real progress signals, learndash_quiz_completed and learndash_lesson_completed, feed a recommendation function that combines the most recent quiz result with learndash_get_course_steps()’s view of what’s left, and asks generate_text() for exactly one of “continue” or “review” plus a reason. The result is stored as visible, dismissible advice, never a change to what content a student can access or complete. The next lesson builds a similarly human-reviewed layer for grading free-text answers.

Resources & further reading

← Building an AI Tutor That Answers From Course Content Auto-Grading Free-Text Answers and Giving Feedback →