Completion is a decision, not automatic
SCORM does not detect completion. The course decides it and reports it. Whatever the content treats as finished — the last page viewed, a quiz passed, a video watched to the end — it writes to the LMS through a small JavaScript runtime API, into a single status field. The LMS stores the value it was handed and nothing else.
The LMS is a recorder, not an observer. It unzips the package, reads the manifest, launches the page in a frame, and waits. It cannot see which slide the learner is on or whether the quiz was answered. It knows only what the content told it, in the vocabulary the specification allows.
Which field holds that answer depends on the version you built:
| Version | Element | Allowed values |
|---|---|---|
| SCORM 1.2 | cmi.core.lesson_status | passed, completed, failed, incomplete, browsed, not attempted |
| SCORM 2004 | cmi.completion_status | completed, incomplete, not attempted, unknown |
| SCORM 2004 | cmi.success_status | passed, failed, unknown |
SCORM 1.2 collapses "did they finish" and "did they pass" into one field, so a course cannot cleanly report both. SCORM 2004 splits them, which is why a 2004 course can report completed and failed at once — a learner who reached the end and got 40%. The reporting consequences are covered in completion status vs success status.
Finding the runtime API
Before the content can report anything, it has to find the object the LMS put in the browser for it. The LMS creates a JavaScript object named API for SCORM 1.2 or API_1484_11 for SCORM 2004, and places it somewhere in the window hierarchy above the frame holding the course. The content walks up to find it:
function findAPI(win) {
var depth = 0;
while (win.API_1484_11 == null && win.parent != null && win.parent != win) {
if (++depth > 500) return null; // give up rather than loop forever
win = win.parent;
}
return win.API_1484_11 || null;
}
// try the parent chain first, then the opener chain
var api = findAPI(window) || (window.opener ? findAPI(window.opener) : null);
This search is the first thing that can fail, and it fails silently. If the LMS launches the SCO in a window shape the content did not expect — a popup with no opener reference, a nested frameset, a cross-origin iframe the script cannot read across — the loop finishes empty. The course still renders and the learner still clicks through, but nothing is reported, and the attempt stays not attempted forever. A course that "works but never tracks" is almost always this.
The full set of methods on that object, and what each returns, is walked through in the SCORM API explained.
The calls that set completion
Once the API object is in hand, the session follows a fixed shape. Every value crossing the boundary is a string — there are no numbers or booleans in the data model, only text the LMS parses.
Initialize("") → "true"
GetValue("cmi.entry") → "resume"
GetValue("cmi.completion_status") → "incomplete"
...learner works through the course...
SetValue("cmi.location", "module-4/page-7") → "true"
SetValue("cmi.score.raw", "84") → "true"
SetValue("cmi.score.scaled", "0.84") → "true"
SetValue("cmi.success_status", "passed") → "true"
SetValue("cmi.completion_status", "completed")
Commit("") → "true"
Terminate("") → "true"
Three things in that sequence matter more than they look.
Initializeopens the session, once. Until it returns"true", everySetValueis invalid and is rejected. Calling it twice in one session is also an error on strict platforms, and everything after the second call is discarded.- Order matters on some platforms. A number of LMSs treat the attempt record as final the moment they first see
completedorpassed, and ignore later writes. Writing score first, then success, then completion, then committing, keeps the record intact on those platforms and changes nothing on the rest. Terminateends the session. Any call after it is invalid — content that writes a final status from an unload handler firing afterTerminateis writing into a closed session.
Reading matters as much as writing. A course that resumes properly checks cmi.entry for "resume" and reads back cmi.completion_status and cmi.location, so it does not overwrite an existing completion with an incomplete on reopening.
Commit and why it matters
SetValue does not save anything durably. It hands a value to the LMS's runtime layer, which typically holds it in the browser or in memory on the server session. Commit is the call that asks the LMS to write the accumulated values to its database. The specification says Terminate should perform an implicit commit, and most platforms honour that — but "most" is doing a lot of work in that sentence, and Terminate only runs if the content gets the chance to call it.
SetValue and a successful Commit, the LMS has nothing to store. The course behaved perfectly; the record is empty.The safe pattern is to commit as progress is made — after each page or module, and again immediately after the status write. Two constraints pull against committing constantly. Some LMSs throttle Commit, honouring one call every few seconds and dropping the rest, so a burst in the final second can lose the one carrying the completion. And unload handlers are unreliable by design; on mobile they are frequently cancelled outright, so a course that commits only on unload loses completions on phones specifically. The full list of ways this goes wrong is in why SCORM completions go missing.
Why do identical courses differ?
Two courses built from the same source deck, imported into the same LMS, can report completely differently. Nothing is broken in either. The difference is in choices the content made that the LMS never sees:
- The completion rule itself. One package sets
completedwhen the final slide renders. Another waits for a quiz score above a threshold. A third requires every page to have been visited. All three are conformant. - Whether success is written at all. A 2004 package that sets
cmi.completion_statusbut nevercmi.success_statusleaves success asunknown, and a report filtered on "passed" will show nobody. - Commit timing. Commit-as-you-go versus commit-on-exit is invisible in the course and decisive when a tab is closed abruptly.
- Resume behaviour. A package that writes
incompleteunconditionally at launch will overwrite an existing completion every time the learner reopens the course to review it. - Version. The same course built as 1.2 has one status field to work with; built as 2004 it has two, and the LMS report reads them differently.
The LMS side varies too. Some platforms derive lesson_status from the score against a mastery score when the content did not set it explicitly. Some coerce passed to completed for rollup. Some truncate cmi.suspend_data past the cap — 4,096 characters in SCORM 1.2, up to 64,000 in 2004 — without reporting an error, so a course that stores its progress state there resumes from corrupted data and cannot tell it has finished.
Reading the runtime log
All of this is guesswork until you look at the actual calls. A runtime log — every API call the package made, with arguments, return values, and error codes — turns "the LMS is not tracking" into a specific line. Work through it in this order:
- Is there an
Initialize, and does it return"true"? No entry at all means the API object was never found. An entry returning"false"means the LMS refused the session. - Is there exactly one? A second
Initializeinvalidates everything after it on strict platforms. - Find the
SetValuethat writes status. Check the element name against the version you built, and check the value against the allowed vocabulary.SetValue("cmi.completion_status", "complete")is a type error — the token iscompleted. - Is there a
Commitafter it that returns"true"? If the status write is the last thing beforeTerminatewith no commit in between, you are relying on the platform's implicit commit. - Check the error codes on any call that returned
"false". In SCORM 1.2,301means the API was not initialised and405means the value was the wrong data type; SCORM 2004 uses a wider, differently numbered set for the same categories. A non-zero code on the status write is your answer. - Confirm
Terminateis last. Anything after it was discarded.
Do this once before the package goes near a learner, and once against the destination platform, because how an LMS launches the SCO and how aggressively it throttles commits varies more than the specification admits.
Where SCORM Central fits
SCORM Central converts files you already have — PDF, PowerPoint, Word, or video — into SCORM 1.2, SCORM 2004, cmi5, or xAPI, and shows you the manifest, the full runtime log, and a conformance report on every build, so you can see exactly what a package will report before an LMS does. See how it works, or start with one file.