LifterLMS Shortcodes: The Complete Reference With Examples
Every shortcode in LifterLMS 10.3 core, grouped by job, with its real attributes, defaults and a working example, plus when to use a block instead…
Read articleSooner or later every LifterLMS developer needs to know where the data actually lives. You might be building a custom report, migrating students in from another platform, debugging an enrollment that looks wrong, or deciding which tables to index on a busy site.
This article maps the LifterLMS database as it stands in version 10.3: the post types that hold content and commerce, the meta keys that link them, the custom tables that hold enrollments, progress and quiz attempts, and the user meta used for caching. It ends with how to query that data without fighting the plugin.
Table names below use the default wp_ prefix. Yours may differ. Everything here was checked against the plugin source; still, treat direct database access as a last resort and prefer the PHP API where one exists.
In short, LifterLMS stores data in three places:
Model classes wrap all of this. LLMS_Course, LLMS_Lesson, LLMS_Order, LLMS_Student and the rest read and write the right rows for you, which is why the PHP API is almost always the better entry point.
| Post type | Holds |
|---|---|
course | Courses |
section | Sections inside a course |
lesson | Lessons |
llms_quiz | Quizzes |
llms_question | Quiz questions |
llms_membership | Memberships |
llms_access_plan | Pricing options attached to a course or membership |
llms_order | Orders |
llms_transaction | Individual payments and refunds against an order |
llms_coupon, llms_voucher | Coupons and vouchers |
llms_engagement | Engagement rules (trigger, type, delay) |
llms_email | Engagement email templates |
llms_achievement, llms_my_achievement | Achievement templates and awarded achievements |
llms_certificate, llms_my_certificate | Certificate templates and awarded certificates |
llms_review | Course reviews |
Courses use four taxonomies: course_cat, course_tag, course_difficulty and course_track. Memberships have their own category and tag taxonomies.
LifterLMS does not use post_parent for the course outline. It uses post meta, and every meta key a model manages is prefixed with _llms_. So the parent_course property of a section is stored as _llms_parent_course.
_llms_parent_course._llms_parent_section, _llms_parent_course and _llms_order (its position), plus _llms_quiz when it has a quiz._llms_lesson_id; a question stores _llms_parent_id pointing to its quiz._llms_product_id, the course or membership it sells.Because the same relationship is stored on both sides in places, moving lessons with raw SQL is risky. Use LLMS_Lesson::set() or the Course Builder so both sides stay in sync.
An llms_order post is the agreement: who bought which access plan, at what price, with which gateway. Its post status is the order status, such as llms-pending, llms-active, llms-completed, llms-on-hold, llms-pending-cancel, llms-cancelled, llms-expired, llms-refunded or llms-failed.
Order details live in meta, again with the _llms_ prefix: user_id, product_id, plan_id, total, currency, payment_gateway, order_key, billing fields, trial fields, date_next_payment, date_access_expires and gateway references such as gateway_customer_id and gateway_subscription_id. Order notes are stored as WordPress comments on the order.
Each payment or refund is an llms_transaction post with its own statuses: llms-txn-succeeded, llms-txn-pending, llms-txn-failed and llms-txn-refunded. Its _llms_order_id meta links it to the order. When a transaction succeeds, LifterLMS moves the order to active (recurring) or completed (one-time) and enrollment follows. Our article on building a custom payment gateway walks through that flow in code.
The most important custom table is wp_lifterlms_user_postmeta. Each row joins a user to a post with a key, a value and an updated_date. It has indexes on user_id and post_id.
| meta_key | Value | Meaning |
|---|---|---|
_status | enrolled, expired, cancelled | Enrollment status in a course or membership |
_start_date | yes | Its updated_date is the enrollment date |
_enrollment_trigger | For example order_123 | What caused the enrollment |
_is_complete | yes or no | Completion of a lesson, section, course or track |
_completion_trigger | Text | What marked it complete |
_achievement_earned, _certificate_earned | ID of the awarded post | Links an awarded engagement to the student and related post |
_email_sent | ID of the email | Record of a sent engagement email |
Two behaviours matter when you query it:
_status rows rather than replacing the old one. The current status is the newest row, which is how LLMS_Student::get_enrollment_status() reads it: ordered by updated_date and then meta_id, descending.updated_date. Drip schedules based on days after enrollment use it, so migrations that create enrollments must set it correctly.| Table | Stores |
|---|---|
wp_lifterlms_quiz_attempts | One row per attempt: student, quiz, lesson, start and end dates, status, attempt number, grade, and the questions and answers as serialized data |
wp_lifterlms_lesson_time_sessions | Lesson time tracking (added in 10.1): heartbeat-based sessions per user and lesson with accumulated seconds |
wp_lifterlms_events | Activity events such as sign-on, sign-out, session start and end, and page load, exit, focus and blur on course content |
wp_lifterlms_events_open_sessions | Pointers to sessions that are still open |
wp_lifterlms_notifications | Queued and sent notifications: type, subscriber, status, trigger, user and post |
wp_lifterlms_sessions | Front-end session data such as checkout notices, with an expiry |
wp_lifterlms_vouchers_codes, wp_lifterlms_voucher_code_redemptions, wp_lifterlms_product_to_voucher | Voucher codes, redemptions and the products each voucher unlocks |
The events and notifications tables grow with activity. On large sites they are usually the first tables worth watching, which our guide to scaling LifterLMS covers.
LLMS_Student stores its own values in user meta with an llms_ prefix. Progress is cached this way: get_progress() saves results under keys such as llms_course_456_progress. The Student Progress Cache tool under LifterLMS > Status > Tools & Utilities clears the overall progress and grade caches (llms_overall_progress and llms_overall_grade); they are rebuilt on the next read and do not change real progress.
Membership restrictions are held in _llms_restricted_levels user meta. Aggregate course data, such as _llms_average_progress, _llms_average_grade and _llms_enrolled_students, is calculated by a background processor and saved on the course. Settings live in the options table with lifterlms_ and llms_ prefixes; payment gateway settings use llms_gateway_{id}_, and permalink bases are in llms_permalinks.
Use the models first. To list students enrolled in a course with their progress and enrollment date:
$course_id = 456;
$query = new LLMS_Student_Query(
array(
'post_id' => $course_id,
'statuses' => array( 'enrolled' ),
'per_page' => 100,
'page' => 1,
)
);
foreach ( $query->get_students() as $student ) {
printf(
"%s,%s,%s\n",
$student->get( 'user_email' ),
$student->get_enrollment_date( $course_id, 'enrolled', 'Y-m-d' ),
$student->get_progress( $course_id, 'course' )
);
}
For reporting across thousands of students, a direct, read-only query can be far faster. This one counts course completions per course in the last 30 days:
SELECT upm.post_id AS course_id, COUNT(*) AS completions
FROM wp_lifterlms_user_postmeta AS upm
INNER JOIN wp_posts AS p ON p.ID = upm.post_id AND p.post_type = 'course'
WHERE upm.meta_key = '_is_complete'
AND upm.meta_value = 'yes'
AND upm.updated_date >= DATE_SUB( NOW(), INTERVAL 30 DAY )
GROUP BY upm.post_id;
In PHP, run queries like this through $wpdb->prepare() with $wpdb->prefix rather than a hard-coded prefix. The LifterLMS Developer Hub documents the model classes in its code reference, and the REST API under /wp-json/llms/v1 exposes students, enrollments, progress and grades for external systems; see our REST API and webhooks introduction.
_status row skips the hooks that send emails, award engagements, update caches and fire webhooks. Use llms_enroll_student(), llms_unenroll_student() and llms_mark_complete().In wp_lifterlms_user_postmeta, as _status rows. The newest row for each user and course is the current status.
No. LifterLMS has its own llms_order and llms_transaction post types. Sites that sell through the WooCommerce integration add-on have WooCommerce orders as well.
Yes. Each attempt row in wp_lifterlms_quiz_attempts stores the questions, the answers given and the grade.
Before writing any custom query, check whether a model method or REST route already returns what you need. If it does not, query read-only, prepare every value, and test against a copy of production data.
If you are planning a custom report, a data migration or a performance fix that touches these tables, our LifterLMS development team works with this schema every day. Tell us what you are building and we will suggest the safest approach.
Every shortcode in LifterLMS 10.3 core, grouped by job, with its real attributes, defaults and a working example, plus when to use a block instead…
Read article
A code walk-through for creating students and enrollments in LifterLMS from a CRM, sales tool or custom app: look up by email, create the account…
Read article
What LifterLMS 10.x registers with the WordPress Abilities API, how an AI agent can discover and run those abilities over REST or MCP, and the…
Read articleSkip the trial and error. Get specialist LifterLMS help and ship faster.