cmi5.xml
and builds the course model it will use later.🧭 Why package structure breaks courses before launch
Most developers first notice cmi5 when they reach launch, tokens, or xAPI statements. That is understandable because those are the moving parts you can see in a browser and trace in logs. But many production failures start earlier, at import time, when the LMS reads
cmi5.xml
and decides what kind of course it is dealing with. By the time an Assignable Unit, or AU, launches, the LMS has already built a model of the course hierarchy, identified which nodes are launchable, and attached rules that affect completion and success.
This is why the package is not just a transport shell. In cmi5, the package is part of the runtime contract. That is one of the clearest practical differences in xAPI vs cmi5. With plain xAPI, you can send well-formed xAPI statements to an LRS without a standardized LMS launch model. With cmi5, the LMS imports structure first, then uses that structure to drive launch behavior. If you want the canonical flow, the cmi5 overview makes this clear: import comes before launch, and launch behavior depends on what was imported.
A useful mental model is simple:
package -> LMS import -> course graph -> AU launch
. That middle step matters more than many teams expect. If the course graph is malformed or semantically weak, the AU can be technically correct and still behave incorrectly in production. Completion can fire too early. A launch URL can resolve in one system and fail in another. Reporting labels can become inconsistent. What looks like a runtime bug often turns out to be a structure bug. Understanding that shift saves time because it changes where you debug first.
Why this matters in practice is simple. The LMS is making durable decisions during import. It may store the course title for catalog display, keep AU IDs for later registration context, and decide which nodes are meaningful for progress calculation. If those values are wrong, fixing only the content application does not repair the imported model. Teams often lose days debugging JavaScript that was never the root cause. A cleaner approach is to treat package authoring like contract authoring. You validate the declaration first, then the behavior that depends on it.
🧱 What the LMS actually imports
At the center of cmi5 packaging is the course structure document. When packaged as a ZIP, the canonical file is
cmi5.xml
. That XML must conform to the official
CourseStructure.xsd
schema. This matters because package import is not just a file upload problem. It is a schema validation and interpretation problem. A package can be zipped correctly and still fail because the XML violates the structure contract. The spec and schema are the first places to look, not the browser console.
The cmi5 specification also requires LMS support for multiple package forms, including standalone XML, 32 bit ZIP, and 64 bit ZIP. That detail from the official specification text matters in practice because LMS interoperability bugs often hide in package handling assumptions. One system may accept a raw XML file for early testing, another may only be exercised by a ZIP import path, and a third may expose archive layout bugs that never appear in local development.
For developers, the important insight is that import is a parsing phase that builds internal state. The LMS does not merely store the uploaded file and wait for launch. It extracts metadata, creates a course hierarchy, associates identifiers, and decides which elements are launchable. That internal representation becomes the source of truth for catalog display, learner navigation, launch selection, and sometimes even reporting labels. If the imported structure is weak, everything downstream inherits that weakness. This is why early validation against the schema is cheaper than runtime investigation later.
Think about what an LMS needs in order to do its job. It needs a course object to show to administrators and learners. It needs a tree of child nodes so it can render navigation. It needs AU definitions so it can know which URLs are launchable. It may also need progression semantics so it can decide whether a registration is still in progress or effectively done. All of that comes from the import stage. The XML is therefore not just descriptive content. It is the source material for the LMS’s internal course model, and any ambiguity in that source becomes ambiguity in the learner experience.
🗂️ The minimum valid shape of a cmi5 course
The smallest cmi5 course is more structured than many people expect, but the rules are not complicated once you see them. The root is
courseStructure
. Inside that there must be exactly one
course
element. After that, the top level can contain one or more
au
or
block
elements. A block can itself contain nested blocks or AUs. The official CourseStructure.xsd makes these constraints explicit, and they are worth reading directly because many import bugs come from violating rules that seem small but are not optional.
An AU is the actual launchable unit. In schema terms, each
au
requires an
id
,
title
,
description
, and
url
. That is the operational core of a cmi5 package. A block, by contrast, is structural. It groups content and can express hierarchy, but it is not itself the thing learners launch. That distinction matters because teams sometimes think in flat launch lists while cmi5 thinks in course structure. The LMS is not just collecting URLs. It is importing a graph with semantic meaning.
Here is the useful takeaway: course metadata describes the course as a whole, block metadata organizes instructional units, and AU metadata controls actual launch behavior. That separation is why cmi5 course structures are more expressive than a spreadsheet of titles and links. You can model a multi lesson course with grouped units, nested sections, and different progression rules before any JavaScript runs. If you skip that mental model, you end up authoring packages that look valid but tell the LMS an incomplete story.
It also helps to read the XML as three layers. The outer layer names the course. The middle layer organizes content into sections or blocks. The inner layer identifies launchable units. Once you think in those layers, the schema feels much less arbitrary. Each part answers a different LMS question: what is this course, how is it organized, and what can the learner actually start? Those questions are basic, but if your package does not answer them clearly, each LMS will fill in the gaps differently. That is where portability starts to erode.
<courseStructure xmlns="https://w3id.org/xapi/profiles/cmi5/v1/CourseStructure.xsd">;
<course id="https://example.com/course/security-basics">
<title><langstring lang="en-US">Security Basics</langstring></title>
<description><langstring lang="en-US">Intro course</langstring></description>
<au id="https://example.com/au/lesson-1">
<title><langstring lang="en-US">Lesson 1</langstring></title>
<description><langstring lang="en-US">First lesson<langstring></description>
<url>launch/index.html</url>
</au>
</course>
</courseStructure>
Reading that example line by line reveals why the minimum structure matters. The root
courseStructure
declares the document type the LMS is about to parse. The
course
element gives the LMS a single logical course container and a stable identifier for it. The nested
title
and
description
provide learner facing and admin facing metadata. Finally, the
au
tells the LMS there is one launchable unit, with its own identity and a URL that the LMS can later resolve. Even this tiny file is not just metadata. It is enough information for an LMS to build a basic course graph and prepare a launch path.
🆔 IDs are not labels, they are part of interoperability
One of the easiest mistakes in cmi5 authoring is treating identifiers as decorative. They are not. Course, block, objective, and AU identifiers are IRIs, and they must be unique within the course structure. The schema and spec both lean on this because those IDs do more than help a human recognize a node. They become stable references that downstream systems use for association, storage, launch context, and reporting.
This matters because weak ID strategy creates long tail problems. If your team uses environment specific values like
test-au-1
, then later changes them in production, your LMS and LRS data become harder to reconcile. If you reuse human friendly slugs across multiple packages, imports may still validate locally but migrations and analytics comparisons become fragile. Good identifiers should be stable, globally unique, and designed for persistence. That sounds like API design because it is similar to API design. A bad OpenAPI schema can mislead every consumer of a service. A bad cmi5 ID strategy can mislead every consumer of the course structure.
The practical effect is easy to miss during development because the first import may appear fine. The damage shows up later when the same AU is versioned, moved between systems, or linked to statements in reporting. If the IDs were not built to last, historical data becomes harder to compare and launch mappings become brittle. For teams planning SCORM to xAPI migration or SCORM to cmi5 modernization, this is one of the quietest but most important mindset changes: structure data is not temporary metadata. It is part of the cross system contract.
A useful developer habit is to separate display text from persistent identity on purpose. Let titles be edited by instructional designers when wording changes. Let IDs remain stable unless the thing itself has truly become a different AU or course node. That separation protects reporting continuity. It also reduces accidental breakage during routine content refreshes. If your analytics, registrations, or administrator workflows depend on a node, changing the title should not make the system forget what that node was.
🎯 Why AU metadata is operational, not descriptive
An AU element carries more than a title and a URL. It is where cmi5 turns generic xAPI capable content into LMS launchable content with defined behavior. In addition to required fields, an AU may define
launchParameters
,
entitlementKey
,
activityType
,
moveOn
,
masteryScore
, and
launchMethod
. These are not cosmetic annotations. They shape how the LMS launches the AU and how it evaluates learner progress after launch.
Consider xAPI launch in the broader xAPI vs cmi5 discussion. Plain xAPI does not standardize a complete LMS launch workflow. cmi5 does, and AU metadata is part of that standardization. Or take
masteryScore
. It is optional, but if present it must be between 0 and 1, and the LMS includes it in launch data. That means package metadata can influence AU runtime logic even before the AU sends a single statement. If a content team changes that score without understanding the AU’s assumptions, they can alter pass fail behavior by configuration alone.
The official complex sample is useful because it shows AUs with different semantic roles. One uses
moveOn="CompletedOrPassed"
and a
masteryScore
of
1.0
. Another uses
moveOn="Passed"
with a much lower mastery threshold. Another stays at
NotApplicable
. You can inspect that directly in the official complex example. Those settings define what the LMS will consider sufficient for each AU. They are behavior controls authored at the package level, not just metadata for display.
The easiest way to understand this is to imagine two AUs with identical HTML and JavaScript but different package metadata. One AU may count as complete after a simple completion signal. Another may require a passing outcome and a defined mastery threshold. From the learner’s point of view, the content might look the same. From the LMS’s point of view, the expectations are different because the package says they are different. That is why AU metadata belongs in architecture discussions, not only packaging checklists. It directly affects the rules the platform applies around your content.
⚠️ The moveOn default that causes surprising completion bugs
The most important default in this part of cmi5 is also the one most likely to be overlooked:
moveOn
defaults to
NotApplicable
. On paper, that can sound harmless. In production, it can change the course’s meaning. The cmi5 best practices material warns that if all AUs remain
NotApplicable
, some LMS behavior can result in the course being treated as satisfied as soon as the learner is registered, before any AU is launched.
This is why so many teams misdiagnose a reporting bug as a tracking bug. They see a learner marked complete too early and start inspecting xAPI statements, launch tokens, or AU code. But the real issue is that the package never told the LMS which learner actions should matter. The LMS can only enforce the semantics it was given. If the package says every AU is effectively outside progression logic, the LMS may conclude there is nothing to wait for.
Why this matters goes beyond completion status. It affects reporting trust. Once stakeholders see satisfied courses with no real learner activity, confidence in the whole implementation drops. That undermines adoption because the system appears unreliable even when the content app is functioning correctly. The safer approach is to author
moveOn
intentionally for every AU that participates in progression and to test the imported structure, not only the launch path. The best practices guidance is especially valuable here because it connects authoring choices directly to LMS behavior.
A practical rule is to make
moveOn
an explicit authoring decision, never an omitted field that you plan to revisit later. If an AU is informational and should not control progression, say so deliberately. If it must be completed, passed, or either completed or passed, declare that at package time. This does two things. First, it makes your package easier to review because the intent is visible in XML. Second, it prevents hidden defaults from shaping learner outcomes without anyone noticing. Defaults are convenient during development, but in interoperability work they often become silent design choices.
This is also where terms like cmi5 moveOn and cmi5 masteryScore matter in search intent. Developers looking for those details are usually troubleshooting real completion behavior, not reading theory. The package values directly influence what the LMS expects the AU to achieve.
🌐 URL rules change depending on packaging mode
Another common source of interoperability pain is AU URL handling. The rules are simple once you know them, but easy to violate if you carry over assumptions from other packaging systems. If the course is delivered as standalone XML, AU URLs must be fully qualified. If the course is delivered as a ZIP, locally packaged launch files should use relative URLs, because the LMS decides where the archive is unpacked. Remotely hosted AUs in a ZIP can still use fully qualified URLs.
This distinction matters because the same content can appear to work in one environment and fail in another based on import strategy alone. A developer might test a local package with hard coded absolute file assumptions, only to find that another LMS unpacks to a different internal location. Or a team may ship standalone XML with relative URLs, expecting the LMS to infer a base path it does not actually have. Those are not edge cases. They are routine portability failures.
The deeper lesson is that cmi5 supports both packaged assets and cross domain delivery, but only if the package tells the truth about where launchable content lives. That flexibility is one of the reasons cmi5 matters in modern delivery architectures. It supports local content, remote content, and mixed models. But flexibility increases the need for precise package design. The spec’s URL rules are there because ambiguous launch resolution produces exactly the kind of inconsistent behavior teams blame on LMS quality when the real problem is package semantics.
A concrete example helps. Suppose your AU files live inside the ZIP under
lesson1/index.html
. In that case, the URL should be relative so the LMS can resolve it after unpacking, regardless of the storage path it chooses internally. By contrast, if your AU is hosted at a public web address such as
https://content.example.com/lesson1/index.html
, the package should say exactly that. Mixing these models carelessly causes launch failures that are hard to reproduce across vendors because each LMS has its own storage layout and import workflow. Clear URL authoring removes that ambiguity before the learner ever clicks Start.
🏗️ Blocks, hierarchy, and the course graph the LMS builds
Blocks are where cmi5 moves beyond a flat list of launch links. A block can contain AUs, nested blocks, or both. That means the package can express a multi lesson hierarchy directly. For developers, this matters because the LMS is not simply showing files. It is building a course graph with parent child relationships and implied progression structure. Even if your first course only has one AU, understanding blocks early helps you avoid authoring patterns that do not scale.
Imagine a two block course. Block 1 contains foundational lessons. Block 2 contains assessments. The assessments may use stricter progression semantics such as
moveOn="Passed"
plus a
masteryScore
, while reference material in Block 1 could be informational and use a different rule. In that model, the structure itself communicates intent. The LMS can render the catalog, learner navigation, and completion state in a way that reflects the instructional design without needing to inspect the AU internals.
This is why the package should be treated like a declarative contract. A well modeled hierarchy lets the LMS behave predictably across platforms. A poorly modeled hierarchy forces you to rely on custom LMS behavior or human interpretation. That is exactly what cmi5 is trying to reduce. If xAPI alone is flexible to the point of ambiguity, cmi5 course structure narrows that ambiguity in the LMS launch scenario. Structure is how that narrowing begins.
There is also a maintenance benefit. Once a course grows from one lesson to ten, then to fifty, hierarchy becomes the only sane way to preserve clarity. Blocks let you add meaning without changing AU internals. You can group onboarding lessons separately from assessments, optional references, or remediation units. That matters for long term system health because structure changes are easier and safer when the package already models the course as a graph instead of a flat list. Good hierarchy gives both the LMS and the humans maintaining the package a shared map.
When developers search for assignable unit cmi5, they are usually trying to understand this exact boundary: what is launchable, what is structural, and what the LMS actually tracks as a learner activity node. That distinction affects implementation decisions later.
🧪 Validate early with the schema and CATAPULT
The best debugging workflow for cmi5 package issues is to separate structure validation from launch validation. First validate against
CourseStructure.xsd
. Then upload either the raw XML or the full ZIP to CATAPULT CTS. This catches a different class of problems than browser debugging. Missing required elements, invalid attribute values, malformed archive layout, and package form issues can be surfaced before you spend time on authentication, launch tokens, or statement flows.
This matters because cmi5 implementations fail in layers. Import can fail. Launch data can fail. Statement conformance can fail. If you collapse those layers into one debugging session, you end up chasing symptoms instead of causes. A package that never described the course correctly cannot be rescued by perfect AU JavaScript. That is not a harsh rule. It is actually useful because it gives teams a repeatable sequence for narrowing problems quickly.
There is also a practical organizational benefit. Schema validation and CATAPULT checks can be added to build pipelines long before full LMS integration is ready. That lets content engineers catch structural regressions during packaging, not during acceptance testing. For teams used to discovering these defects in late stage QA, the shift is significant. It moves cmi5 package quality from manual troubleshooting into automated contract validation, which is where it belongs.
A sensible workflow looks like this. Generate or edit
cmi5.xml
. Validate the XML against the schema locally or in CI. Package assets into ZIP only after the structure passes. Then run CATAPULT against both the structure and the packaged form. Only after those checks pass should you spend time testing LMS launch and AU runtime behavior. This order matters because each stage answers a different question. Is the XML structurally valid? Is the package form acceptable? Does the LMS import it as intended? Does the AU behave correctly after launch? Clear separation makes failures easier to diagnose and easier to fix.
For search intent, this is where ADL CATAPULT, xAPI conformance testing, and cmi5 conformance connect. Developers often think conformance starts when statements hit the LRS. In practice, packaging conformance starts earlier, at the contract the LMS imports.
🧩 The hidden risk of changing package semantics after import
One subtle interoperability trap is the temptation to “fix” package behavior inside the LMS after import. The cmi5 best practices warn against LMS side edits such as changing
moveOn
values or injecting a
masteryScore
that the AU was not designed around. On the surface, that sounds convenient. In reality, it can break the contract between package authoring and AU runtime behavior.
Why is this so risky? Because the AU may emit only the statements needed for the original authored semantics. If administrators make the completion rule stricter, the AU might never satisfy it. If they insert a mastery threshold, the AU may not calculate or report success the way the LMS now expects. The package can still look valid, and the AU can still launch, but learner outcomes become inconsistent. This is the hardest kind of bug to explain because nothing appears obviously broken in isolation.
There is an architectural lesson here. cmi5 packages are not mere defaults for the LMS to reinterpret freely. They are authored contracts that align structure, launch, and expected reporting behavior. If your implementation requires administrative overrides, test the effective launch data as carefully as the imported XML. The specification allows some flexibility in what gets surfaced at launch, but flexibility without disciplined testing is how interoperable packages turn into platform specific behavior.
This matters for governance as much as for debugging. In many organizations, administrators want to solve urgent business needs by changing a rule in the LMS rather than reauthoring the package. Sometimes that is understandable, but it creates a hidden split between what the package says and what the platform now does. Over time, nobody is sure which behavior is canonical. Reimporting the package, updating the content, or moving to another LMS can then surface conflicts that were masked by local configuration. The safer pattern is to treat package semantics as the source of truth and reserve LMS overrides for exceptional cases that are documented and retested.
💡 A better way to think about cmi5 packages
A useful comparison is API contract design. When an API schema is wrong, the service implementation can be perfect and clients will still integrate incorrectly. cmi5 package structure works the same way. The AU is the executable component, but the course structure defines how the LMS should understand, launch, and evaluate it. If that contract is weak, the system fails even though individual pieces may be valid on their own.
This is why package structure deserves architectural attention, not just production packaging effort. Titles and descriptions influence catalog display and reporting labels. IDs influence traceability and long term data coherence. Blocks influence how the LMS models the course. URL forms influence whether launch is portable.
moveOn
and
masteryScore
influence progression semantics. None of that is superficial. It is all behavior encoded declaratively.
The practical takeaway is simple but important. In cmi5, structure is behavior. If you are diagnosing launch issues, completion anomalies, or inconsistent portability across LMSs, inspect the imported course structure before touching runtime code. Read the schema. Validate the XML. Test in CATAPULT. Then move outward to launch and statements. That order matches how the LMS itself experiences the course. And once you adopt that mental model, a lot of cmi5 “mystery bugs” stop being mysterious.
That perspective changes how teams build as well as how they debug. Package authoring becomes an early engineering task, not a last minute publishing step. Review of IDs, hierarchy, URLs, and progression rules becomes part of design, not only QA. The result is not just cleaner XML. It is more predictable learner outcomes, more portable packages, and less platform specific guesswork. cmi5 can feel complex at first because it asks you to think structurally. But that structure is what turns xAPI flexibility into something an LMS can launch and manage consistently.
Seen this way, the xAPI vs cmi5 question is not really about which standard is better. It is about scope. xAPI is the event and data layer, often tied to an LRS and broader learning analytics xAPI goals. cmi5 is the LMS interoperability layer that constrains launch and progression for managed courses. That difference matters when planning an xAPI data strategy, deciding between LRS vs LMS roles, evaluating xAPI profiles, or designing migration paths from older e-learning standards. Once that distinction is clear, package structure stops looking like overhead and starts looking like the mechanism that makes interoperable course delivery possible.
🔢 #3 of 12 | cmi5








