StrategyBy

Recovering Course Progress After You've Already Cut Over: Merge, Never Overwrite

Most migrations skip historical course progress on purpose — it's expensive to reconstruct and rarely missed. But if you decide later that you want it back, and your students have already been using the new system, you can't just import. You have to merge. Here's the pattern that works.

The situation

You migrated a course catalog from one LMS to another. To keep scope realistic, you skipped historical completion data — students start over in the new system. Three weeks after cutover the client asks: 'Actually, can we get their old progress back? People are emailing about losing their place.' Now the new system has new completion records overlapping the historical ones. A naive import would overwrite real, current activity with old data. The right move is to merge, not import.

What's already in your export (if you used a thoughtful exporter)

Course-level completion data may already be in your exporter output even if your importer never used it. Our Thinkific exporter records `audit.completed`, `audit.completed_at`, and `audit.percentage_completed` on every enrollment record from the REST `/enrollments` endpoint — even when the importer is configured to skip progress. If your exporter is similar, the cheapest win is a one-shot backfill script that reads those fields out of the existing export JSON, no new API calls. That's where to start.

Course-level completion: earliest wins

The relevant LearnDash field is the `course_{id}_completed_on` user meta — a single timestamp per user per course. The safe rule is earliest wins: if a user has a Thinkific completion timestamp and no LearnDash completion, set it. If they have both, keep the earlier one — Thinkific is the real history; LearnDash's is just when they re-confirmed in the new UI. If they have only LearnDash, leave it alone. This preserves real history without erasing anything they've done since cutover.

Per-lesson progress: set union

LearnDash stores per-lesson progress in the `_sfwd-course_progress` user meta — a serialized array per course of which lesson and topic IDs have been completed. Merging is structurally a union: read the existing array, add any Thinkific-completed IDs that aren't already in the LearnDash array, write it back. Nothing gets uncompleted. Nothing gets overwritten. The only cost is pulling per-lesson progress from Thinkific, which requires the source account to still be active and authenticated (the GraphQL `User` type exposes lesson progress, and the REST API has progress endpoints).

Quiz attempts: usually skip

Quiz attempts are append-only in LearnDash by nature — both `wp_learndash_user_activity` (with `_meta`) and the ProQuiz statistics tables hold one row per attempt. Historical Thinkific attempts would coexist as additional rows with older timestamps, no collisions. The blocker is upstream: Thinkific's API surface for individual historical attempts is thin — you can get aggregates but reconstructing individual attempts (timestamps, chosen answers, scores) is incomplete. Unless there's a specific business reason to preserve attempt history, the value-to-effort ratio doesn't justify it.

The pre-flight audit you must run first

Before any merge, run a per-course audit: count how many enrolled users have any LearnDash activity since cutover — `_sfwd-course_progress` non-empty, or `course_{id}_completed_on` set, or any quiz attempts. Small overlap means the merge is low-stakes and you can proceed. Large overlap means you need a dry-run conflict report listing every disagreement before flipping the switch. The audit is the cheapest piece of work in this whole project and the most important — it tells you how scared to be.

The one real risk

A student who deliberately un-marked or reset something in LearnDash post-cutover. Re-importing the Thinkific completion would silently restore it. In practice this is rare but it does happen — usually someone who wanted to re-do a unit. The mitigation is logging: every meta-update the merge script makes goes to a per-user log, so if a student emails support saying 'why is this lesson marked done again?' you can find the merge action and reverse it manually. Don't try to detect this case automatically; the cost of false positives is higher than the cost of resolving a handful of human-flagged conflicts.

Realistic time estimate

Stop after the course-level backfill (audit + earliest-wins merge from data already in your export) and you've recovered roughly 80% of the value for 4–6 hours of work. Full per-lesson progress requires new exporter code, an importer for the merge, a dry-run mode, and testing — budget 12–18 hours. Quiz attempts add another 8–12 hours if you have to negotiate Thinkific's API for individual attempts, which is rarely worth it. The 80/20 hit is the course-level backfill; everything beyond is incremental.

Last updated: