🧩 The bug is usually not your statement syntax
A cmi5 integration can look healthy on the surface. The cmi5 package imports, the assignable unit cmi5 launch works, the browser opens the right screen, and xAPI statements appear in the learning record store. That makes it tempting to assume the hard part is done. Then the real symptoms show up. Learners appear completed before they finish. Relaunches behave like brand new attempts. Review mode creates duplicate outcomes. A dashboard says everything is fine while support tickets say the opposite.
When that happens, teams often inspect the visible edges first. They look at JSON structure, verb IDs, auth headers, cross-domain launch issues, or token exchange. Those checks matter, but many production defects are not caused by malformed statements. They come from a quieter design mistake. The implementation has flattened three different scopes into one. In cmi5, the Assignable Unit, the registration, and the session are connected, but they are not the same thing. If your code stores them in one table row, one cache key, or one mental model, the resulting bugs spread everywhere.
The AU is the launchable content boundary. The registration is the learner’s longer-lived enrollment scope. The session is one specific AU run inside one launch. Those boundaries are what let cmi5 answer practical questions cleanly. What content is this? Which enrollment does it belong to? Which launch attempt produced this event? If your system cannot answer those separately, completion logic becomes noisy, resume behavior becomes unreliable, and learning analytics xAPI pipelines stop reflecting learner reality.
This matters because cmi5 is not just xAPI plus a launch URL. It adds a defined lifecycle so LMSs and content can behave consistently across vendors. That difference sits at the center of xAPI vs cmi5. The specification is explicit about launch data, statement ordering, registration continuity, and session boundaries. Good implementations follow those boundaries all the way through storage and reporting. If you want the spec-level reference points, start with the cmi5 overview, the LMS flow, and the AU flow. Once you read them through the lens of scope, a lot of confusing failures stop looking random.
🧠 The three-scope model that keeps cmi5 sane
The cleanest way to think about cmi5 is this: registration is the enrollment scope, AU is the content scope, and session is the launch-attempt scope. That sounds simple, but it changes system design in concrete ways. It affects how you persist state, how you decide whether a learner is done, how you debug relaunches, and what your reports are actually measuring.
The AU answers, “What piece of launchable content is this?” It is the unit the LMS can assign, launch, and evaluate. The registration answers, “Which learner enrollment does this work belong to?” It persists across launches and keeps continuity when the learner leaves and returns. The session answers, “Which specific runtime instance produced these statements?” It gives you one bounded execution window with a beginning and an end.
That separation matters because each scope changes at a different rate. The AU is relatively stable. It represents a defined activity and its cmi5 moveOn expectations. The registration is stable across time for one learner and course context. The session is volatile. It starts when the AU is launched and ends when the AU terminates or is abandoned. Mixing stable identifiers with volatile ones creates state drift. For example, if you use a session identifier where registration continuity is needed, resume breaks. If you use registration-level outcomes to represent one browser attempt, reliability diagnostics disappear.
Developers coming from SCORM sometimes expect a tighter coupling between runtime and attempt state. cmi5 is more deliberate. It preserves the fact that one learner may interact with the same AU several times under one enrollment. That is why completed, passed, and failed have a different operational meaning from initialized and terminated. Some signals describe the lifecycle of the enrollment’s progress through the AU. Others describe what happened in this specific launch. If you do not preserve that distinction, your implementation may still send valid xAPI, but it will stop behaving like valid cmi5. This is one reason SCORM to cmi5 work often surfaces architecture issues that stayed hidden in older content models.
📦 Assignable Units are not just files you can launch
An Assignable Unit is the separately launchable unit of cmi5 content and the primary unit of tracking and management. That definition is easy to skim past, but it carries architectural weight. Many teams treat the AU as a packaging artifact, almost like a folder entry that points to an HTML file. In practice, it is the boundary where completion, satisfaction, score, duration, launch mode, and learner experience need to make sense together.
That is why AU sizing is not an afterthought. If an AU is too large, one unit may contain several different learning experiences with different progress semantics. Resume logic becomes fragile because the content has to reconstruct too much state. Completion becomes ambiguous because the AU is doing more than one thing. Imagine a single AU containing a video lesson, a branching scenario, and a scored assessment. If the learner completes the video and exits before the assessment, what exactly does “completed” mean? The larger the AU, the easier it is for those semantics to become muddy.
If an AU is too small, you get the opposite problem. Reporting becomes fragmented across tiny units, relaunch overhead increases, and learners bounce between screens that should probably have been one experience. More importantly, you move meaning out of the AU and into course-level aggregation. That weakens cmi5’s design because cmi5 moveOn and satisfaction are intended to operate on meaningful launchable units, not arbitrary slices that only make sense when stitched back together later.
The practical test is simple. Can one AU produce completion, success, score, and duration data that mean something on their own? If not, the package structure is probably fighting the model. The cmi5 best practices material is helpful here because it frames the AU as the place where these signals should align. That matters for the long-term health of the system. Good AU boundaries make resume easier, moveOn more predictable, and reporting easier to explain to non-engineers. Bad AU boundaries create confusion that no amount of statement cleanup can fully fix. In broader e-learning standards work, this is where content design and interoperability meet.
🪪 Registration is the long-lived truth
Registration is one of the most important identifiers in cmi5, and also one of the easiest to misuse. The LMS creates the registration for the learner’s enrollment context and passes that identifier into AU launches. That means the registration is not a browser event and not a one-time token. It is the continuity thread that ties repeated launches together into one meaningful learning record.
This has direct implications for how content behaves. Suppose a learner launches an AU on Monday, reaches 40 percent progress, and exits. On Wednesday they relaunch from another device. If that second launch uses the same registration, the AU should treat the learner as continuing the same enrollment-level experience, not starting an unrelated one. Resume state, prior completion, previous pass or fail outcomes, and moveOn evaluation all have to be interpreted through that longer-lived registration scope.
This is also where many duplicate-outcome bugs come from. Teams emit completed, passed, or failed based only on what happens in the current tab. That sounds reasonable until you remember that these outcomes are registration-sensitive. If the learner already satisfied the AU under the same registration, re-emitting the same outcome during a review launch can distort analytics and confuse LMS behavior. The AU should inspect registration-level history before deciding whether a new outcome is actually warranted.
For reporting, registration is the layer that answers business questions. Did the learner satisfy this AU for this enrollment? Did they eventually pass? Was completion reached before a deadline? Session data cannot answer those questions alone because one session may contain only partial work. Registration data gives the durable truth. That is why flattening everything into latest-launch reporting causes so much confusion. You lose the continuity that cmi5 intentionally preserves. This is also where the distinction between LRS vs LMS becomes practical. The LMS manages assignment and launch context, while the learning record store preserves the evidence used to evaluate that context over time.
🚀 A session is one AU attempt, and ordering matters
A session is one runtime instance of one AU launch. It begins when the LMS launches the AU and ends when that AU terminates. That sounds obvious, but it is one of the most useful boundaries in the model because it gives you reliable telemetry about what happened in a specific attempt.
In cmi5, statement ordering is not just cosmetic. Before content runs, the LMS records a launched statement. Once the AU starts, the AU must send initialized as its first statement and terminated as its last statement for that session. Other allowed statements happen between those two boundaries. This is one of the clearest ways to distinguish true cmi5 lifecycle behavior from generic xAPI activity that merely happens to launch in a browser. For teams comparing xAPI launch patterns with cmi5, this is one of the strongest reasons the wrapper matters.
Why does the ordering matter so much? Because it determines whether your telemetry is trustworthy. If initialized is missing, you cannot prove the AU actually started. If terminated is missing, you cannot tell whether the learner exited cleanly, lost connectivity, or closed the tab without warning. If progress or completion statements appear outside those bounds, the statement stream may still be valid xAPI, but it no longer describes a valid cmi5 session in a dependable way.
This becomes especially important in debugging. A package that works in a happy-path demo may fail badly under interruptions, retries, or relaunches. Automated lifecycle checks are therefore more valuable than one-time visual tests. The CATAPULT CTS is useful because it exposes the ordering across launches and registrations, making it easier to catch missing initialized or terminated statements before your users do. In practice, this is part of both xAPI conformance testing and cmi5 conformance.
🔑 Launch data tells the AU how to behave
The cmi5 launch contract is not just a transport mechanism. It tells the AU who the learner is, where to report, which enrollment this launch belongs to, and how the LMS expects this launch to behave. Parameters such as
endpoint
,
fetch
,
actor
,
registration
, and
activityId
are operational instructions, not optional decoration.
The
endpoint
points the AU to the LRS. The
fetch
URL is used to retrieve temporary authorization details for the running launch. The
actor
identifies the learner. The
registration
ties the launch to the enrollment-level record. The
activityId
identifies the AU activity being experienced. Each parameter answers a different runtime question, and misusing any one of them can corrupt state. For example, treating
fetch
as a reusable general API path instead of a launch-scoped token retrieval endpoint can cause avoidable authorization failures across relaunches.
Launch data also includes
LMS.LaunchData
, which contains values such as launchMode. This is one of the most underappreciated parts of cmi5 because it directly affects AU behavior. In Normal mode, the launch is typically credit-bearing. In Review mode, the learner may be revisiting already completed content. In Browse mode, evaluative behavior may need to be limited or fully suppressed depending on design. If the AU ignores launchMode and simply reacts to UI events, it can easily emit outcomes that are technically valid but contextually wrong.
That matters because interoperability depends on respecting both the data and the intent of the launch contract. A review launch should not casually create a second completed or passed statement under the same registration unless that behavior is truly intended. A browse launch should not accidentally alter completion state just because the learner clicked through screens. The LMS is providing context so the AU can behave predictably. Ignoring that context is one of the fastest ways to make a cmi5 integration look correct in isolation while producing misleading records in production. For teams working on xAPI for developers, this is where cmi5 becomes a practical governance layer rather than just another transport pattern.
https://content.example.com/launch?
endpoint=https%3A%2F%2Flrs.example.com%2Fxapi%2F&
fetch=https%3A%2F%2Flms.example.com%2Ffetch%2Fabc123&
actor=%7B...%7D&
registration=550e8400-e29b-41d4-a716-446655440000&
activityId=https%3A%2F%2Fexample.com%2Fcourse%2Fau%2Fintro
✅ Completion is evaluated across the registration, not per tab
This is the point where many teams realize why their reports drifted. A valid session is not the same thing as a satisfied AU. The learner can launch an AU, make partial progress, and terminate correctly. That session is complete from a runtime perspective, but the registration may still be incomplete from a learning perspective. cmi5 separates those ideas on purpose.
The AU becomes satisfied only when the statements produced under the registration meet the AU’s moveOn rule. That means course truth emerges from registration-level evaluation, not from whether one browser tab happened to open and close cleanly. If your reporting logic assumes one launch equals one completion decision, it will be wrong as soon as learners resume, review, or split their work across multiple sessions.
Consider a common case where moveOn is CompletedAndPassed. In the first session, the learner starts the AU, reaches partial progress, and exits. In the second session, using the same registration, the AU restores prior state and the learner finishes successfully. The AU then emits completed and passed before terminated. Satisfaction is reached across the registration only at that moment, not during the first session and not simply because the AU had been launched before.
This is why duplicate completion events are such a common bug. Developers often emit completed or passed whenever the learner reaches a familiar UI state, without checking whether that outcome was already recorded earlier in the same registration. The result is noisy analytics and unpredictable LMS interpretation. The sample scenarios are helpful because they show how course truth depends on accumulated registration evidence rather than the latest launch alone. This has direct consequences for xAPI data strategy because data quality depends more on scope discipline than on statement volume.
Session 1, registration R1:
launched -> initialized -> progressed(40) -> terminated
Session 2, same registration R1:
launched -> initialized -> completed -> passed -> terminated
📈 Progress telemetry and moveOn are related, but not the same
Progress and completion often get blurred together, but cmi5 treats them differently for good reason. Progress is in-flight telemetry. It tells you how far the learner has moved through the AU during work that is still underway. Completion is a distinct outcome that signals the AU is done in the relevant sense for that unit.
Best practices recommend using the progressed verb with the progress extension while the learner is actively working, and they specifically recommend not sending progress 100. Once the learner reaches the end state, the AU should emit completed instead. This distinction protects the meaning of both signals. If progress 100 and completed are used interchangeably, dashboards and business rules start making contradictory assumptions about whether the AU is merely advanced or actually finished.
moveOn adds another layer. It is AU metadata used by the LMS to decide whether the AU is satisfied. So the content can send perfectly valid xAPI and still fail to satisfy the AU if the emitted outcomes do not align with moveOn. If the AU requires CompletedAndPassed but the content only emits progress and termination, syntax validation will pass while learner satisfaction never occurs. That kind of failure is frustrating because the data looks active, yet the enrollment never reaches the expected state.
There is a second trap here. If moveOn is omitted or effectively left as NotApplicable across all AUs, some LMS behavior may cause the course to be considered satisfied immediately on registration creation. Teams sometimes interpret that as an LMS defect when it is really a package design problem. The cmi5 best practices call this out because it has a direct real-world impact. Learners can appear finished before they launch anything, which destroys trust in the reporting layer from day one. Similar design mistakes also affect SCORM to xAPI migration work when old completion assumptions are carried forward without rethinking the AU boundary and satisfaction model.
💾 Resume state belongs to the registration, not the session
Resume is where scope mistakes become visible to learners immediately. If a learner leaves and comes back, the AU needs a durable source of truth that survives the browser tab, the device, and the timing of the launch. In cmi5, the strongest pattern is to store resume data using the xAPI State API keyed in a registration-aware way, while treating session memory as temporary runtime state.
The reason is straightforward. A session is short-lived by design. It exists only for one AU run. When the browser closes, in-memory state disappears. Registration is what persists across launches, so it is the natural anchor for restored progress, last-known location, prior branching decisions, or incomplete interaction state that should carry forward. If you anchor resume to session instead, every relaunch risks looking like a first attempt.
There is also a subtler failure mode. If resume is stored too broadly, such as by learner alone without the right AU and registration context, the AU may restore the wrong state. That can produce strange and hard-to-reproduce bugs where a learner returns to the wrong screen or sees old assessment conditions from another enrollment. To the learner, that feels like random brokenness. To the engineer, it is usually a scoping bug hidden inside persistence strategy.
A practical pattern is to load registration-scoped state during initialization, keep high-frequency interaction details in memory during the running session, and periodically flush meaningful checkpoints back to durable state storage. That balance matters for resilience. If the learner loses a tab unexpectedly, the next launch can still restore recent progress. Resume data and session telemetry then do their separate jobs cleanly. One tells the AU what should survive. The other tells your systems what happened during this specific run. This is one area where both cmi5 for developers and xAPI profiles thinking can help teams make state behavior explicit instead of implicit.
⚠️ Abandoned sessions expose reliability problems you will otherwise miss
Not every session ends neatly with terminated. Learners close laptops, mobile operating systems kill apps, browsers crash, networks drop, and corporate security tools interrupt requests. cmi5 acknowledges that reality. If a previous AU session did not end cleanly, the LMS may issue an abandoned statement before a later relaunch. That detail is easy to ignore until you need to explain why learners complain about instability while completion reports still look acceptable.
Abandoned sessions matter because they reveal a class of failures that registration-level success data hides. A learner may eventually pass the AU after several retries, but the path there may have included broken launches, lost progress windows, or repeated token and state-recovery issues. If you only look at final completion, you miss the reliability story. Session-level analysis tells you whether the runtime experience is healthy, not just whether learners eventually got through it.
From an engineering perspective, abandoned sessions are diagnostic gold. A high abandoned rate can indicate late state flushes, fragile unload handlers, oversized assets, or launch initialization that fails under real latency. It can also reveal that the AU depends too heavily on clean shutdown behavior. That matters for long-term system quality because resilient learning software should tolerate interruptions without turning every disruption into learner frustration.
This is why logging both registration ID and session ID throughout your platform is essential. In statement inspectors, app logs, support traces, and analytics pipelines, registration tells you which enrollment the work belongs to. Session ID tells you which launch attempt broke. Without both, investigation turns into guesswork. You may know a learner had trouble, but you cannot isolate whether the problem happened during launch, during progress tracking, or during shutdown.
{
"actor": { "objectType": "Agent", "account": { "homePage": "https://lms.example.com", "name": "user-42" } },
"verb": { "id": "http://adlnet.gov/expapi/verbs/progressed", "display": { "en-US": "progressed" } },
"object": { "id": "https://example.com/course/au/intro" },
"context": {
"registration": "550e8400-e29b-41d4-a716-446655440000",
"extensions": {
"https://w3id.org/xapi/cmi5/context/extensions/sessionid": "9f4c4fd1-83d0-4d9b-8ec4-17f8b3d9e100"
}
},
"result": {
"extensions": {
"https://w3id.org/xapi/cmi5/result/extensions/progress": 40
}
}
}🧪 Test the lifecycle, not just the package import
A surprising number of cmi5 projects are declared successful the moment a package imports and an AU opens. That is the beginning of testing, not the end. The real value of cmi5 appears when you validate the full lifecycle across repeated launches, partial progress, resume, review mode, and interrupted sessions under the same registration.
A useful test matrix mirrors real learner behavior instead of ideal behavior. Start with a first launch that initializes correctly and records partial progress. Then relaunch under the same registration and confirm that resume state is restored. After completion, test a Review mode launch and verify that it does not create duplicate evaluative outcomes. Finally, simulate an interrupted session that never sends terminated and confirm that later relaunch handling and abandoned-session visibility still make sense.
Tools like the CATAPULT player and CTS are especially useful because they let you see registration logs and session events together. That is exactly what you need when debugging scope problems. A package-level import check cannot tell you whether completion was duplicated across relaunches or whether initialized and terminated are consistently bounded around session activity.
The broader lesson is that conformance is not only about passing a checklist. It is about proving that your implementation keeps its promises under real usage. The cmi5 best practices guide is valuable here because it connects specification rules to production realities such as AU sizing, moveOn behavior, statement timing, and relaunch handling. Those are the places where healthy demos turn into unhealthy deployments if you stop testing too early. For teams using ADL CATAPULT, this is where tooling becomes part of a practical quality process rather than a final checkbox.
🏗️ The implementation rule that prevents most downstream confusion
If you take one architecture rule from all of this, make it this: model AU, registration, and session as separate first-class concepts in code, storage, and reporting. Do not reuse one identifier for another. Do not collapse enrollment state into attempt state. Do not infer long-lived completion truth from one session’s telemetry.
In practical terms, that usually means separate storage concerns. Registration-level records should answer whether the learner satisfied the AU, whether they completed it, and whether they passed or failed for that enrollment context. Session-level records should capture launch timing, initialized and terminated boundaries, interruptions, and granular runtime telemetry. The AU itself should remain the stable content boundary that ties launch metadata, moveOn configuration, and activity identity together.
This separation pays off quickly. Debugging becomes simpler because each layer answers a different class of question. Product managers get reports that distinguish learner success from launch reliability. Support teams can isolate whether a complaint came from resume restoration or from one broken session. Engineers can change runtime instrumentation without corrupting completion records, and they can revise registration logic without rewriting launch telemetry pipelines.
That is why scope is not a theoretical concern. It is what keeps your data honest over time. A package can launch successfully while still producing misleading outcomes, duplicate completions, and unusable analytics. cmi5 gives you a clear lifecycle model so that does not happen. If you preserve AU scope, registration scope, and session scope all the way through your implementation, the rest of the system becomes easier to reason about. If you flatten them, the platform may still run, but its records will slowly stop telling the truth.
🔍 Why this matters in xAPI vs cmi5 decisions
For teams deciding between raw xAPI and cmi5, the difference is not that one is modern and the other is old. The real difference is operational discipline. xAPI gives you a flexible way to capture experience data, define xAPI profiles, and build a broader xAPI data strategy. cmi5 adds a launch contract, runtime expectations, and completion rules that make assigned learning content behave predictably inside an LMS.
That matters because many organizations do not actually struggle with sending statements. They struggle with knowing which statements count, when they count, and how to keep reporting consistent across vendors. In that sense, xAPI vs cmi5 is often really a question of flexibility versus governed interoperability. If your use case is assigned courseware with resumable launches, consistent completion, and auditable enrollment logic, cmi5 provides structure that plain xAPI does not enforce on its own.
For migration work, this also clarifies the path forward. SCORM to xAPI migration can improve observability, but SCORM to cmi5 is often the better fit when you need launch, assignment, and LMS-facing completion semantics to survive the transition. Add requirements such as cmi5 masteryScore, moveOn alignment, and vendor portability, and the case becomes more concrete. The key is not choosing the newer acronym. It is choosing the model that matches how your learning system actually needs to behave.
🔢 #4 of 12 | cmi5








