A client emails at 9pm: "Everyone finished the course but the LMS still says incomplete — did you build it wrong?"You open the package, click straight to the last slide, watch the completion screen appear, and it looks perfect. So you reply "works on my end," and you are both now stuck. The course runs. The completion just never registers. Nine times out of ten the culprit is a single SCORM value: cmi.completion_status.
It is the most misunderstood field in the whole SCORM envelope, and it breaks more "why is this not completing?" tickets than the rest of the spec combined. So let's take it apart: what it means, what it is not, how each authoring tool sets it, how to read the live value in thirty seconds, and the safe default that makes most of these tickets disappear.
Completion is not the same as passing. cmi.completion_statusanswers "did they finish?" — never "did they pass?"— the sentence that closes most of these ticketsWhat the value actually is.
cmi.completion_status is a SCORM 2004 data-model element. The course writes it through the runtime API (SetValue), the LMS reads it back, and that one string is what decides whether a learner's record flips to "complete." The spec allows exactly four values, and nothing else is legal:
completed— the learner finished the experience.incomplete— they started but haven't reached the end yet.not attempted— they've never meaningfully engaged with the SCO.unknown— the runtime can't determine status. This is the one that ruins your evening.
Here is the part people miss: a course that never explicitly writes completed will sit at incomplete (or unknown) forever, no matter how many times a learner clicks to the final slide. Completion is an assertion the course has to make, not something the LMS infers from the last page being visited. If the trigger that fires that assertion is on the wrong slide, behind a button no one clicks, or never authored at all, the value never changes.
Visiting the last slide does not equal completion. The course has to call SetValue("cmi.completion_status", "completed") and then Commitit. If your authoring tool's completion trigger never runs, the LMS has nothing to read and the learner stays "incomplete" for eternity.
Completion is not success.
The single biggest source of confusion is that people treat "completed" and "passed" as the same event. In SCORM 2004 they are two separate fields with two separate jobs:
cmi.completion_statusanswers "did they finish?" — valuescompleted/incomplete.cmi.success_statusanswers "did they pass?" — valuespassed/failed.
A learner can finish a quiz and fail it: that's completed + failed, which is a completely valid combination. SCORM 1.2 couldn't express that nuance — it crammed everything into one field, cmi.core.lesson_status, whose values (passed, failed, completed, incomplete, browsed, not attempted) mix the two concepts together. That single design decision is why so many courses behave differently across SCORM versions. When you migrate a course from 1.2 to 2004, you are splitting one field into two, and any logic that assumed they were the same thing breaks.
How each tool sets it.
You rarely write cmi.completion_statusby hand — your authoring tool does it, based on a "completion trigger" you configure at publish time. The defaults differ in ways that quietly cause tickets, so here's what each common tool does.
Articulate Storyline.
Trigger: a slide visited, a result slide, or a custom JavaScript call
In the publish dialog, the Reporting and Trackingoptions let you complete by number of slides viewed, by a result slide's pass/fail, or via a "Course is complete when the user…" trigger you wire yourself. The classic bug: tracking is set to a result slide the learner can finish the course without ever reaching, so completion never fires.
What's good
- Completion trigger is an explicit, visible setting at publish time
- Can complete on a result slide, a specific slide, or your own trigger
- Publishes cleanly as either SCORM 1.2 or 2004
What's not
- Default trigger is often "last slide viewed" — easy to point at the wrong slide
- A custom completion trigger that never fires fails silently
Articulate Rise.
Trigger: percent of content viewed, or quiz pass
Rise keeps it deliberately simple: complete when the learner views a set percentage of the lesson, or when they pass a quiz. That removes the wrong-slide trap but introduces a subtler one — a learner who scrolls fast can trip a percent-viewed completion without actually engaging.
What's good
- Completion is a simple percent-of-blocks-viewed or quiz-pass toggle
- Hard to point at the "wrong slide" — there are no slides to mis-target
- Good default behavior for linear content
What's not
- Percent-viewed completion can fire before a learner reads anything
- Less control when you need a precise, custom completion moment
Adobe Captivate.
Trigger: slide views, quiz, or a project completion setting
Captivate gives you completion by slide views or by quiz result. The frequent failure mode is publishing as one SCORM version and hosting on a runtime that expects the other — the completion callgoes out, but to a field name the host isn't listening on, and the status stays unknown.
What's good
- Flexible completion criteria (slide views, quiz, or both)
- Granular reporting options for compliance work
What's not
- Version mismatches between published package and host bite hard here
- Quiz-pass completion sometimes conflates success with completion
Authoring in SCORM 2004 (which writes cmi.completion_status) and uploading to a host that only speaks SCORM 1.2 (which reads cmi.core.lesson_status). The course runs, the learner finishes, and the status never updates because the host is listening on a field the course never writes. Always match the publish version to what your host supports — and when in doubt, test the value live.
Read the value in 30 seconds.
Stop guessing. You can read the live completion status straight from the browser console while the course is open, which tells you instantly whether the problem is the course (it never wrote completed) or the host (it isn't storing what the course wrote). Open the module, finish it, then open DevTools and ask the running API directly.
// Find the SCORM 2004 runtime the course is talking to. // (1.2 courses expose "API"; 2004 courses expose "API_1484_11".) var api = window.API_1484_11 || window.top.API_1484_11; // What did the course actually assert? api.GetValue("cmi.completion_status"); // "completed" | "incomplete" | "not attempted" | "unknown" api.GetValue("cmi.success_status"); // "passed" | "failed" | "unknown" // Force a terminal state to confirm the host stores it, then reload. api.SetValue("cmi.completion_status", "completed"); api.Commit("");
If GetValue returns completed but the LMS report still says incomplete, the course is doing its job and the host (or a sync delay) is the suspect. If it returns incomplete or unknown after you genuinely finished, the course never fired its trigger — go back to the publish settings. This one check collapses most arguments between an ID and an LMS admin into a five-minute fix.
The "API not found" trap
One more wiring gotcha worth naming: SCORM courses search the parent window for the API during page load. If your host injects that API afterthe course has finished looking, you get a silent "API not found," and nothing the course writes — including completion — is ever recorded. A good host injects the API before the course frame loads. If GetValue throws because apiis null, that's your sign.
The safe default.
After enough of these tickets, the prescription gets short. For new content, default to SCORM 2004 4th Edition: it separates completion from success, gives you a far larger suspend_data budget for resume, and supports a completion threshold so you can tie completion to real progress instead of a single slide. Then:
- Set the completion trigger to something a learner mustreach — a final result slide or a true end screen, not "last slide in the menu."
- Keep completion and success distinct. Don't mark a quiz "complete" only on a pass unless that's genuinely your rule — failing learners still finished.
- Match your publish version to your host. If you host on a platform that runs SCORM 1.2, 2004, and xAPI in the browser — like a purpose-built hosting service — the version-mismatch failure mode disappears entirely.
- Before you ship, read the live value from the console once. Thirty seconds now saves the 9pm email later.
Completion status isn't mysterious once you see it for what it is: a single string the course has to assert and the host has to store. Get the trigger right, match the versions, and verify the value — and "why is this not completing?" stops being a recurring ticket. If you're still untangling where to put the course in the first place, the SCORM hosting walkthrough covers the four real options.
Frequently asked questions.
What's the difference between cmi.completion_status and cmi.core.lesson_status?
They belong to different SCORM versions. cmi.core.lesson_status is SCORM 1.2 and folds completion and pass/fail into one field. cmi.completion_status is SCORM 2004 and tracks only completion; success lives in a separate cmi.success_status field. Upload a 2004 course to a 1.2-only host and the names never match, so completion silently never fires.
Why does my course say incomplete even after I finish it?
Usually the course never wrote a terminal value, or it wrote one the host ignores. Common causes: the completion trigger is on the wrong slide, the API was injected after the course gave up looking for it, or the value is stuck at "unknown" because the package version and host version disagree. Read the live value in the console before blaming the host.
What values can cmi.completion_status hold?
Four, defined by the SCORM 2004 spec: "completed", "incomplete", "not attempted", and "unknown". Anything else is invalid. If you see "unknown", the runtime could not determine status — almost always a version or wiring problem, not a learner problem.
Should I author in SCORM 1.2 or 2004 to avoid completion bugs?
SCORM 2004 4th Edition is the safer default for new content. It separates completion from success, gives a larger suspend_data budget, and supports completion thresholds. Keep a SCORM 1.2 export on hand only if a specific client LMS demands it.
/06 Try it for free
Drop a SCORM file. See it live in 11 minutes.
Free for 3 modules. No card. Lifetime is $149 once. You read the article, you know how this is supposed to work — see it in your own browser.
About the author
Jon Vig · Ex-LMS engineer · founder.
How to host a SCORM file (without an LMS).
Tutorial · 14 min read
How to actually host a SCORM file. Without an LMS.
Free SCORM hosting that actually works in 2026.
Tutorial · 8 min read
The five "free SCORM hosting" tools, ranked.
21 instructional designer portfolio examples we steal from.
Examples · 12 min read