Claude Skill

learndash-course-access

Build or audit LearnDash course access and enrollment logic. Use when code mentions sfwd_lms_has_access, ld_update_course_access, ld_course_access_from, ld_course_access_from_update, ld_course_access_expired, learndash_user_get_enrolled_courses, course_*_access_from, open/free/pa

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download lonsdale201-wp-agent-skills-learndash_learndash-course-access-52f6020.zip · 3 KB
Part of lonsdale201/wp-agent-skills — 226 skills

Install

skills CLI npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/learndash/learndash-course-access
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
Git git clone https://github.com/Lonsdale201/wp-agent-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

LearnDash course access

Use this when a plugin, theme, migration, checkout bridge, import, or custom frontend needs to decide whether a user can access a LearnDash course or must grant/revoke direct course enrollment.

Do not reduce LearnDash access to one usermeta key. In LearnDash 5.1.9, course access can come from direct enrollment, group enrollment, open courses, sample steps, admin auto-enroll, free/paynow settings, start/end dates, expiration rules, and legacy access-list mode.

When to use this skill

Trigger when ANY of these appears:

  • sfwd_lms_has_access(), ld_update_course_access(), ld_course_access_from(), ld_course_access_from_update(), ld_course_access_expired(), learndash_get_users_for_course(), or learndash_user_get_enrolled_courses().
  • Usermeta keys like course_123_access_from, learndash_course_123_enrolled_at, or learndash_course_expired_123.
  • A custom course page, lesson gate, import, CRM sync, Woo bridge, membership bridge, or REST endpoint that grants/removes course access.
  • A bug report says "user is enrolled but cannot access", "group users cannot see course", "expired users still appear", or "open course behaves weirdly".

Access decision chain

sfwd_lms_has_access( $post_id, $user_id ) is the canonical yes/no gate. It resolves the course for the passed post ID and then applies these broad rules:

  1. No related course can mean access allowed for non-course content.
  2. Missing course post/status means no access.
  3. Admin auto-enroll, sample steps, and open courses can grant access without usermeta enrollment.
  4. Some free/paynow configurations can grant access without an explicit user.
  5. Direct access uses course_{$course_id}_access_from usermeta.
  6. Group access is checked through learndash_user_group_enrolled_to_course().
  7. Direct non-group access also respects the Product model start/end window.
  8. Final access can be revoked by ld_course_access_expired().
  9. The result is filterable through sfwd_lms_has_access.

If the task is authorization, call the access API. Do not query only course_{$course_id}_access_from, because group and open-course access will be missed.

Grant and revoke correctly

Modern LearnDash exposes LearnDash\Core\Models\Product for course/group products. It delegates to the public access helpers and applies product-level filters.

use LearnDash\Core\Models\Product;

$product = Product::find( $course_id );

if ( $product ) {
    $product->enroll( $user_id );
    // $product->unenroll( $user_id );
}

The lower-level public helper is still the important compatibility surface:

ld_update_course_access( $user_id, $course_id );        // grant
ld_update_course_access( $user_id, $course_id, true );  // revoke

ld_update_course_access() does more than write usermeta:

  • On grant, it writes course_{$course_id}_access_from if missing.
  • It records learndash_course_{$course_id}_enrolled_at as historical enrollment time.
  • It creates/updates LearnDash user activity with activity_type = access.
  • It fires learndash_update_course_access.
  • It deletes the learndash_user_courses_{$user_id} transient.
  • On revoke, it intentionally does not delete learndash_course_{$course_id}_enrolled_at, because reports use it.

Use ld_course_access_from_update( $course_id, $user_id, $timestamp ) when the task is changing the access start timestamp. It updates both access activity and the direct access usermeta. Do not write that meta directly unless you are doing a one-off repair and also handle cache/activity consequences.

Read access state

Use the smallest API that matches the question:

  • "Can this user access this course/lesson/topic now?" -> sfwd_lms_has_access( $post_id, $user_id ).
  • "When did direct course access start?" -> ld_course_access_from( $course_id, $user_id ).
  • "When does access expire?" -> ld_course_access_expires_on( $course_id, $user_id ).
  • "Which courses is this user enrolled in?" -> learndash_user_get_enrolled_courses( $user_id ).
  • "Which users have this course?" -> learndash_get_users_for_course( $course_id ).

Important query caveats:

  • Open courses can return all users from learndash_get_users_for_course() unless admin users are excluded by the query settings.
  • learndash_user_get_enrolled_courses() merges auto-enroll, open courses, legacy access-list courses, direct access meta, and group courses.
  • In 5.1.9, the $bypass_transient parameter in learndash_user_get_enrolled_courses() is effectively forced to true before the function rebuilds the list. Do not build logic that depends on a cached response from that parameter.

Expiration and start/end dates

Course expiration is handled by ld_course_access_expired() and ld_course_access_expires_on(). When an access record is expired and processing is allowed by learndash_process_user_course_access_expire, LearnDash marks learndash_course_expired_{$course_id}, revokes direct course access, fires learndash_user_course_access_expired, and may delete progress depending on course settings.

The Product model adds course start/end behavior:

  • Product::has_started() and Product::has_ended() affect direct enrolled paid/closed courses.
  • Open products are not blocked by product start/end dates.
  • Extended course access can override normal end-date behavior.
  • Group-derived access can supply the effective start timestamp for a user.

If a custom plugin grants access from an order/import and needs a historical date, set the access-from date through ld_course_access_from_update() after granting access.

Course structure is not access

Creating sfwd-lessons, sfwd-topic, or sfwd-quiz posts does not attach them to a course structure. Course step order lives in LearnDash's course steps model and related metadata. Use the course step APIs or REST /steps endpoint when a task must build a usable course outline.

For course outline reads, use:

  • learndash_get_course_steps( $course_id )
  • learndash_course_get_steps_count( $course_id )
  • learndash_course_get_steps_by_type( $course_id, $post_type )
  • learndash_course_get_children_of_step( $course_id, $step_id )

Common mistakes to reject

  • Checking only course_{$course_id}_access_from to decide access.
  • Granting/revoking by direct update_user_meta() or delete_user_meta().
  • Removing learndash_course_{$course_id}_enrolled_at during unenroll.
  • Treating group enrollment as direct course enrollment.
  • Assuming a course REST create/update request also creates the course outline.
  • Ignoring course access expiration after a positive direct-meta check.
  • Hooking custom logic only to post save when the real access source is Woo, REST, group changes, imports, or background jobs.

Cross-references

  • Use learndash-group-access for group membership, group-course links, hierarchical groups, and group leaders.
  • Use learndash-woocommerce-access for Woo order/subscription/refund driven enrollment and LearnDash WooCommerce counters.
  • Use learndash-rest-api for REST endpoints that enroll users, update course steps, or expose course data to headless clients.
  • Use learndash-course-progress for step completion, course status, resets, progress activity, and incomplete-step navigation.

References

Validated against LearnDash LMS 5.1.9 local source:

  • includes/course/ld-course-user-functions.php
  • includes/ld-users.php
  • src/Core/Models/Product.php
  • includes/course/ld-course-steps-functions.php
  • includes/classes/class-ldlms-model-course-steps.php
  • Official documentation: https://developers.learndash.com/
Files (wp-agent-skills)
  • agents
    • openai.yaml 209 B
      interface:
        display_name: "LearnDash Course Access"
        short_description: "LearnDash course access and enrollment"
        default_prompt: "Use $learndash-course-access to review LearnDash course enrollment logic."
      
  • SKILL.md 8.2 KB
    ---
    name: learndash-course-access
    description: >-
      Build or audit LearnDash course access and enrollment logic. Use when code
      mentions sfwd_lms_has_access, ld_update_course_access,
      ld_course_access_from, ld_course_access_from_update,
      ld_course_access_expired, learndash_user_get_enrolled_courses,
      course_*_access_from, open/free/paynow/closed course price types, access
      expiration, course start/end dates, course access activity, or custom
      course-gated templates.
    metadata:
      wp-skills-author: "Soczó Kristóf"
      wp-skills-contact: "mailto:lonsdale201@hotmail.com"
      wp-skills-plugin: "sfwd-lms"
      wp-skills-plugin-version-tested: "5.1.9"
      wp-skills-php-min: "7.4"
      wp-skills-last-updated: "2026-08-06"
    ---
    
    # LearnDash course access
    
    Use this when a plugin, theme, migration, checkout bridge, import, or custom
    frontend needs to decide whether a user can access a LearnDash course or must
    grant/revoke direct course enrollment.
    
    Do not reduce LearnDash access to one usermeta key. In LearnDash 5.1.9, course
    access can come from direct enrollment, group enrollment, open courses, sample
    steps, admin auto-enroll, free/paynow settings, start/end dates, expiration
    rules, and legacy access-list mode.
    
    ## When to use this skill
    
    Trigger when ANY of these appears:
    
    - `sfwd_lms_has_access()`, `ld_update_course_access()`,
      `ld_course_access_from()`, `ld_course_access_from_update()`,
      `ld_course_access_expired()`, `learndash_get_users_for_course()`, or
      `learndash_user_get_enrolled_courses()`.
    - Usermeta keys like `course_123_access_from`,
      `learndash_course_123_enrolled_at`, or `learndash_course_expired_123`.
    - A custom course page, lesson gate, import, CRM sync, Woo bridge, membership
      bridge, or REST endpoint that grants/removes course access.
    - A bug report says "user is enrolled but cannot access", "group users cannot
      see course", "expired users still appear", or "open course behaves weirdly".
    
    ## Access decision chain
    
    `sfwd_lms_has_access( $post_id, $user_id )` is the canonical yes/no gate.
    It resolves the course for the passed post ID and then applies these broad
    rules:
    
    1. No related course can mean access allowed for non-course content.
    2. Missing course post/status means no access.
    3. Admin auto-enroll, sample steps, and open courses can grant access without
       usermeta enrollment.
    4. Some free/paynow configurations can grant access without an explicit user.
    5. Direct access uses `course_{$course_id}_access_from` usermeta.
    6. Group access is checked through `learndash_user_group_enrolled_to_course()`.
    7. Direct non-group access also respects the `Product` model start/end window.
    8. Final access can be revoked by `ld_course_access_expired()`.
    9. The result is filterable through `sfwd_lms_has_access`.
    
    If the task is authorization, call the access API. Do not query only
    `course_{$course_id}_access_from`, because group and open-course access will be
    missed.
    
    ## Grant and revoke correctly
    
    Modern LearnDash exposes `LearnDash\Core\Models\Product` for course/group
    products. It delegates to the public access helpers and applies product-level
    filters.
    
    ```php
    use LearnDash\Core\Models\Product;
    
    $product = Product::find( $course_id );
    
    if ( $product ) {
        $product->enroll( $user_id );
        // $product->unenroll( $user_id );
    }
    ```
    
    The lower-level public helper is still the important compatibility surface:
    
    ```php
    ld_update_course_access( $user_id, $course_id );        // grant
    ld_update_course_access( $user_id, $course_id, true );  // revoke
    ```
    
    `ld_update_course_access()` does more than write usermeta:
    
    - On grant, it writes `course_{$course_id}_access_from` if missing.
    - It records `learndash_course_{$course_id}_enrolled_at` as historical
      enrollment time.
    - It creates/updates LearnDash user activity with `activity_type = access`.
    - It fires `learndash_update_course_access`.
    - It deletes the `learndash_user_courses_{$user_id}` transient.
    - On revoke, it intentionally does not delete
      `learndash_course_{$course_id}_enrolled_at`, because reports use it.
    
    Use `ld_course_access_from_update( $course_id, $user_id, $timestamp )` when the
    task is changing the access start timestamp. It updates both access activity and
    the direct access usermeta. Do not write that meta directly unless you are doing
    a one-off repair and also handle cache/activity consequences.
    
    ## Read access state
    
    Use the smallest API that matches the question:
    
    - "Can this user access this course/lesson/topic now?" ->
      `sfwd_lms_has_access( $post_id, $user_id )`.
    - "When did direct course access start?" ->
      `ld_course_access_from( $course_id, $user_id )`.
    - "When does access expire?" ->
      `ld_course_access_expires_on( $course_id, $user_id )`.
    - "Which courses is this user enrolled in?" ->
      `learndash_user_get_enrolled_courses( $user_id )`.
    - "Which users have this course?" ->
      `learndash_get_users_for_course( $course_id )`.
    
    Important query caveats:
    
    - Open courses can return all users from `learndash_get_users_for_course()`
      unless admin users are excluded by the query settings.
    - `learndash_user_get_enrolled_courses()` merges auto-enroll, open courses,
      legacy access-list courses, direct access meta, and group courses.
    - In 5.1.9, the `$bypass_transient` parameter in
      `learndash_user_get_enrolled_courses()` is effectively forced to true before
      the function rebuilds the list. Do not build logic that depends on a cached
      response from that parameter.
    
    ## Expiration and start/end dates
    
    Course expiration is handled by `ld_course_access_expired()` and
    `ld_course_access_expires_on()`. When an access record is expired and processing
    is allowed by `learndash_process_user_course_access_expire`, LearnDash marks
    `learndash_course_expired_{$course_id}`, revokes direct course access, fires
    `learndash_user_course_access_expired`, and may delete progress depending on
    course settings.
    
    The `Product` model adds course start/end behavior:
    
    - `Product::has_started()` and `Product::has_ended()` affect direct enrolled
      paid/closed courses.
    - Open products are not blocked by product start/end dates.
    - Extended course access can override normal end-date behavior.
    - Group-derived access can supply the effective start timestamp for a user.
    
    If a custom plugin grants access from an order/import and needs a historical
    date, set the access-from date through `ld_course_access_from_update()` after
    granting access.
    
    ## Course structure is not access
    
    Creating `sfwd-lessons`, `sfwd-topic`, or `sfwd-quiz` posts does not attach
    them to a course structure. Course step order lives in LearnDash's course steps
    model and related metadata. Use the course step APIs or REST `/steps` endpoint
    when a task must build a usable course outline.
    
    For course outline reads, use:
    
    - `learndash_get_course_steps( $course_id )`
    - `learndash_course_get_steps_count( $course_id )`
    - `learndash_course_get_steps_by_type( $course_id, $post_type )`
    - `learndash_course_get_children_of_step( $course_id, $step_id )`
    
    ## Common mistakes to reject
    
    - Checking only `course_{$course_id}_access_from` to decide access.
    - Granting/revoking by direct `update_user_meta()` or `delete_user_meta()`.
    - Removing `learndash_course_{$course_id}_enrolled_at` during unenroll.
    - Treating group enrollment as direct course enrollment.
    - Assuming a course REST create/update request also creates the course outline.
    - Ignoring course access expiration after a positive direct-meta check.
    - Hooking custom logic only to post save when the real access source is Woo,
      REST, group changes, imports, or background jobs.
    
    ## Cross-references
    
    - Use `learndash-group-access` for group membership, group-course links,
      hierarchical groups, and group leaders.
    - Use `learndash-woocommerce-access` for Woo order/subscription/refund driven
      enrollment and LearnDash WooCommerce counters.
    - Use `learndash-rest-api` for REST endpoints that enroll users, update course
      steps, or expose course data to headless clients.
    - Use `learndash-course-progress` for step completion, course status, resets,
      progress activity, and incomplete-step navigation.
    
    ## References
    
    Validated against LearnDash LMS 5.1.9 local source:
    
    - `includes/course/ld-course-user-functions.php`
    - `includes/ld-users.php`
    - `src/Core/Models/Product.php`
    - `includes/course/ld-course-steps-functions.php`
    - `includes/classes/class-ldlms-model-course-steps.php`
    - Official documentation: <https://developers.learndash.com/>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related