Why Your SCORM Course Isn't Tracking Completions: The 7 Real Causes

A field guide from engineers who repair these for a living — symptom, confirmation, and fix for each cause, in the order we actually check them.

Seven causes account for nearly every SCORM completion failure: a version mismatch in imsmanifest.xml, lesson_status versus completion_status confusion, a mastery score overriding the package's own logic, suspend_data past the 4,096-character limit, API discovery failing in nested frames, the wrong LMS grading method, and authoring-tool reporting settings. Confirm each in the browser console before touching the package.

A learner clicks through every slide, passes the quiz, closes the window — and the LMS says incomplete. If you run compliance training, that's not a cosmetic bug. It's an audit finding waiting to happen.

We repair SCORM tracking for a living, and after enough of these you notice something: it's almost never a mysterious LMS bug. It's one of seven specific, findable, fixable causes. This guide covers all seven with enough detail that you can fix most of them yourself. That's deliberate. If you get stuck, we're here; if you don't, you just saved a few hundred dollars.

First, understand what "completion" actually is

A SCORM package doesn't tell the LMS "the learner finished." It makes JavaScript calls against an API object the LMS exposes in the browser — LMSSetValue("cmi.core.lesson_status", "completed") in SCORM 1.2, or SetValue("cmi.completion_status", "completed") in SCORM 2004. The LMS writes those values to its database, and reports are built from those rows. In Moodle™ that table is mdl_scorm_scoes_track (through 4.2) or mdl_scorm_scoes_value joined to mdl_scorm_element (4.3 and later).

So a "completion not tracking" problem is always one of three breakages: the call was never made, the call was made but rejected or mistranslated, or the value landed but the report interprets it differently than you expect. The seven causes below are just the specific ways each of those happens.

The two-minute triage

Before anything else, open the course, press F12, select the frame that contains the actual course content (not the LMS shell) in the console's frame picker, and paste:

(function () {
  var w = window;
  for (var i = 0; i < 10; i++) {
    if (w.API) return "SCORM 1.2 API found";
    if (w.API_1484_11) return "SCORM 2004 API found";
    if (w.parent === w) break;
    w = w.parent;
  }
  return "No API found";
})();

"No API found" sends you straight to cause 5. An API that's present but the wrong version is cause 1 or 2. If the API is there and correct, the problem is in what's being sent or how it's being scored — causes 3, 4, 6, or 7 — and the database tells you which. In Moodle:

SELECT element, value, timemodified
FROM mdl_scorm_scoes_track
WHERE userid = :userid AND scormid = :scormid
ORDER BY timemodified;

Those rows are the ground truth. Everything below comes back to them.

Get a free SCORM tracking diagnosis

Rather have us look? Send the package or a test login and we'll tell you which of the seven it is — free, and we reply within 1 business hour. Repairs are $350 flat if you want us to finish the job.

Quick reference

Match your symptom first

SymptomMost likely cause
Nothing tracks at all — attempt shows "not attempted"1. Version mismatch or 5. API discovery failure
Score records fine, status stays incomplete2. lesson_status vs completion_status or 7. Authoring-tool reporting settings
Learners finish everything but show "failed"3. Mastery score override
Works in short tests, breaks for real learners mid-course; resume restarts from slide 14. suspend_data over the limit
"Unable to acquire LMS API" alert, or works in SCORM Cloud but not your LMS5. API discovery failure
Status is right in the tracking data but grade is 0 and activity completion never fires6. LMS grading method
Only quiz-passers ever complete; everyone else stuck at incomplete forever7. Authoring-tool reporting settings

1. The version mismatch: your manifest says one thing, your content says another

Every SCORM zip has an imsmanifest.xml at its root, and the LMS believes whatever it says. Open it and find the metadata block:

ADL SCORM
1.2

versus, for 2004:

2004 3rd Edition

A second tell: SCORM 1.2 resources use adlcp:scormtype="sco" (lowercase t), 2004 uses adlcp:scormType="sco". Yes, the capital T matters. Welcome to SCORM.

The failure mode: someone hand-assembled or "converted" a package, the manifest declares 1.2, so the LMS exposes window.API — but the content's runtime JavaScript is 2004 code hunting for window.API_1484_11. It finds nothing, gives up, and not one row is ever written. The course usually still plays, which is what makes this one so sneaky.

Confirm it

The triage snippet reports which API the LMS exposed. Then check the content's own launch file (usually index_lms.html or similar) or the console errors — a 2004 package failing on a 1.2 adapter typically logs its own "API not found" complaint. Zero rows in mdl_scorm_scoes_track for a course the learner demonstrably opened is the database signature.

The fix

Re-export from the authoring tool with the version set correctly, or edit the manifest to declare the version the content actually speaks. Don't try to translate the runtime calls by hand unless you enjoy pain — the data models don't map one-to-one, which brings us to cause 2.

2. lesson_status vs completion_status: one field became three and nobody told your report

SCORM 1.2 has a single status field, cmi.core.lesson_status, with six legal values: passed, completed, failed, incomplete, browsed, not attempted. SCORM 2004 split it into two independent fields: cmi.completion_status (did they finish?) and cmi.success_status (did they pass?). A learner can be completed + failed at the same time in 2004. In 1.2 that state literally cannot be expressed.

The confusion cuts both ways. A 2004 package that only ever sets completion_status looks unfinished to any report keyed on success. A converted package running as 2004 that still calls SetValue("cmi.core.lesson_status", ...) gets error 401 (undefined data model element) on every attempt — the call fails quietly and status never moves.

Confirm it

Look at the element names in your tracking rows. If you see cmi.completion_status rows but your report or completion rule is looking for passed/failed, that's it. If you see a score but no status element of either flavor, the package isn't sending one — skip ahead to cause 7.

The fix

Align the three layers: what the package sends, what the LMS stores, and what the report reads. In Moodle, the activity completion setting "Require status" lets you pick Passed and/or Completed — checking both covers packages that send either. If the package sends nothing useful, the fix is in the authoring tool's reporting settings (cause 7), not the LMS.

3. The mastery score silently overrules your package

This one generates the angriest support tickets, because the package is doing exactly what it was built to do — and the LMS is discarding its answer.

A SCORM 1.2 manifest can carry a mastery score on an item:

80

Moodle has a setting called "Mastery score overrides status" (masteryoverride — it exists site-wide and per activity, and it defaults to on). When a mastery score is present, Moodle recomputes the status from the raw score: at or above 80 becomes passed, below becomes failed — even if the package itself sent completed. Authoring tools sometimes write a mastery score into the manifest that nobody remembers setting, and if your course has an optional quiz or a passing bar different from the manifest's, learners who legitimately finish get stamped failed.

Moodle's separate "Require minimum score" activity completion rule stacks on top of this. Two scoring gates, one authored gate inside the package — three referees, and they don't compare notes.

Confirm it

Open the manifest and search for masteryscore. Then look at the rows: a cmi.core.score.raw below that number next to a lesson_status of failed, on a learner the package's own logic would have completed, is the fingerprint.

The fix

Pick one referee. Either delete the adlcp:masteryscore element from the manifest and re-zip (the package's authored logic then decides), or turn off "Mastery score overrides status" on the activity, or align all thresholds to the same number. We usually recommend letting the package decide — its author knew which questions were graded.

4. suspend_data over 4,096 characters: the invisible ceiling

SCORM 1.2 caps cmi.suspend_data — the string a course uses to remember where the learner was — at 4,096 characters. Modern Storyline and Captivate courses stuff slide states, variables, and quiz progress into that string, and a long course sails past 4,096 without anyone noticing, because short test runs never hit the ceiling. Your ten-minute QA pass looks perfect. Your learner on slide 61 of 80 is the one who loses.

Past the limit, some LMSs reject the write (the LMSSetValue returns "false"), others truncate the string and store it. Both are bad: the course reloads a mangled resume state next session, restarts from the beginning or breaks outright, and the completion that depended on cumulative progress never fires. This is the annoying part — the failure shows up one session after the actual error.

Confirm it

In the console, mid-course: API.LMSGetValue("cmi.suspend_data").length. Anywhere near 4,096 in a 1.2 package is your answer. In the database, check the character length of the cmi.suspend_data value — a row sitting at exactly the limit means something truncated it.

The fix

Republish as SCORM 2004 4th Edition, which raises the ceiling to 64,000 characters — this is the clean fix and usually a ten-minute republish. Storyline 3 and 360 also compress suspend data automatically, which buys a lot of headroom. If you're stuck on 1.2, cut tracked variables, split the course into multiple SCOs, or as a last resort disable resume. Test the full-length course afterward, not the first three slides.

Halfway checkpoint. If none of the first four matched, the remaining three causes live outside the package — in the browser window structure and the LMS configuration. If you'd rather hand it off now: flat $350 per package, diagnosed, repaired, and verified in SCORM Cloud, with a 5-pack at $1,400. The one visible SCORM specialist we know of starts at $650. Get a free SCORM tracking diagnosis — we reply within 1 business hour, or call (615) 396-7139.

5. API discovery fails in nested iframes and popups

SCORM content finds the LMS API by climbing the browser's window tree — the standard ADL findAPI routine checks window.parent, then the parent's parent, and gives up after 7 levels (it also checks window.opener for popup launches). This worked fine in 2004, when a course sat one frame below the LMS. It breaks in three modern ways:

  • Too many layers. An LMS theme wrapper, an embedded player, a portal that iframes the whole LMS — stack enough frames and the content is more than 7 hops from the API. The search gives up.
  • Cross-origin frames. If the package is served from a different domain than the LMS shell (a CDN, a separate content server), the browser throws a SecurityError the moment findAPI touches window.parent. The API might be one frame away; the content is forbidden from looking.
  • Severed popups. A course that opens itself in a new window depends on window.opener — which modern browsers and rel="noopener" policies increasingly null out.

Confirm it

This is the cause when the triage snippet says "No API found," when learners see an "Unable to acquire LMS API adapter" alert, or when the console shows SecurityError / cross-origin frame access errors. The database signature is stark: zero rows, every learner, every attempt. And the package will work flawlessly when you upload it to SCORM Cloud — because the problem was never the package.

The fix

Serve the content from the same origin as the LMS. Flatten the frame stack: in Moodle's SCORM activity, set Display to "Current window" rather than "New window," and stop iframing the LMS inside another portal if you can avoid it. If the content insists on opening its own popup, reconfigure that in the authoring tool's player settings instead. Hosting setup is exactly the kind of thing our managed Moodle hosting gets right by default, for what it's worth.

6. The LMS grading method is counting something your package never sends

Moodle's SCORM activity has four grading methods: Learning objects, Highest grade, Average grade, and Sum grade. "Learning objects" doesn't read scores at all — it counts how many SCOs in the package reported a completed or passed status. For a package that reports a score but never sets a status Moodle counts (see causes 2 and 7), that count is zero. Grade: 0. Forever.

On its own that's a gradebook oddity. It becomes a completion bug when activity completion is set to "Require grade" — the completion rule waits on a grade that can never arrive, so the activity never ticks, course completion never fires, and the certificate downstream never issues. The tracking data, meanwhile, looks fine. Most shops stare at the package for a day before anyone opens the grading dropdown.

Confirm it

Status rows present and correct in the tracking table, but the gradebook shows 0 and the completion checkbox stays empty. That combination is this cause, nearly every time.

The fix

Set the grading method to "Highest grade" for scored content, and drive completion from "Require status: Passed or Completed" instead of the grade. Re-grading existing attempts in Moodle recalculates from the stored tracks, so historical learners get credited too — no one has to retake anything.

7. The package was published to only report status on a quiz pass

Sometimes everything downstream is perfect and the package genuinely never says "completed" — because of one dropdown at publish time.

In Storyline: Publish → Reporting and Tracking. Two settings matter. Tracking decides when status is sent (quiz result, number of slides viewed, or a completion trigger). Reporting decides which words get sent: Passed/Incomplete, Passed/Failed, Completed/Incomplete, or Completed/Failed. The trap combination is "track using quiz result" with "Passed/Incomplete": a learner who views every slide but fails or skips the quiz is reported incomplete — not failed, incomplete — and stays that way for eternity. From the LMS report it's indistinguishable from someone who closed the tab on slide 2.

Captivate has the same trap with different labels: Quiz Preferences → Reporting, where Completion criteria (slide views and/or quiz) and the status representation ("Incomplete → Complete" vs "Incomplete → Passed/Failed") are set separately, and disagree with each other by default more often than you'd hope.

Confirm it

The pattern in the tracking data is unmistakable: every learner with a passing cmi.core.score.raw flips to passed; everyone else has view activity, maybe a failing score, and a status frozen at incomplete. If you have the source file, the publish dialog confirms it in ten seconds.

The fix

Decide what completion should mean for this course — that's a policy question, not a technical one — then set the dropdowns to match and republish. "Completed/Incomplete" tracking slides viewed for awareness content; "Passed/Incomplete" only when a quiz gate is genuinely intended, and say so on the course page so learners know. No source file? The published output can be patched at the JavaScript layer — fiddly, but it's routine work for us and it beats rebuilding the course.

Still stuck? Isolate the variable

One habit will save you hours on any SCORM problem: upload the package to SCORM Cloud (Rustici's test bench — free tier, no excuse not to). It tracks there but not in your LMS? The package is fine; the problem is causes 3, 5, or 6 — LMS-side. Broken in both? Causes 1, 2, 4, or 7 — package-side. That one test cuts the search space in half, and it's the first thing we do on every repair.

  • Tracks in SCORM Cloud, not in your LMS → check mastery override, frame nesting, grading method
  • Broken everywhere → check manifest version, status vocabulary, suspend_data length, publish settings

Questions we get asked about SCORM completion tracking

Why does my course show completed in Storyline preview but not in the LMS?

Preview mode doesn't talk to a real SCORM API — there's no LMS present, so nothing exercises the tracking path at all. Preview only validates slide logic. Always test the published zip in SCORM Cloud or the actual LMS before signing off.

Where does Moodle store SCORM tracking data?

Moodle 4.2 and earlier: the mdl_scorm_scoes_track table, one row per CMI element per attempt. Moodle 4.3+ refactored this into mdl_scorm_attempt, mdl_scorm_element, and mdl_scorm_scoes_value. Query by userid and scormid to see exactly what a package reported.

What's the difference between "passed" and "completed" in SCORM?

Completed means the learner finished the content; passed means they met a success threshold, usually a quiz score. SCORM 1.2 crams both into one field (cmi.core.lesson_status); SCORM 2004 separates them into cmi.completion_status and cmi.success_status, so a learner can be completed and failed simultaneously.

Can you fix a SCORM package without the original source file?

Usually, yes. Manifest problems (version declarations, mastery scores) are direct edits to imsmanifest.xml, and reporting behavior can often be patched in the published JavaScript. Suspend_data overflows are the main case where republishing from source is genuinely better.

Does Moodle fully support SCORM 2004?

Mostly. Moodle plays SCORM 2004 packages and tracks the 2004 data model fine, but it never implemented full 2004 sequencing and navigation. Simple 2004 packages work; packages that depend on complex sequencing rules won't behave. For those, test in SCORM Cloud to see intended behavior.

If you'd rather not spend your afternoon in imsmanifest.xml

Everything above is genuinely enough to fix most tracking failures yourself — that's the point of publishing it. But if this is your third attempt, or the broken course is blocking a compliance deadline, our SCORM tracking repair is $350 flat per package (5-pack $1,400): we diagnose the cause, apply the fix, and hand you verification evidence from SCORM Cloud plus your own LMS's tracking rows showing status landing correctly. If the same package breaks again, the re-fix is free. Migrating LMSs and worried your SCORM data won't survive the trip? That's a different job — see our migration service and its zero-data-loss guarantee, or the full price list.

Get a free SCORM tracking diagnosis

We reply within 1 business hour. Prefer to talk it through? (615) 396-7139.