“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.
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.
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.
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
- learndash_quiz_completed hook reference, developers.learndash.com
- learndash_lesson_completed hook reference, developers.learndash.com
- learndash_get_course_steps() function reference, developers.learndash.com