📦 Create your cmi5 export for an LMS
cmi5 is a standardized xAPI profile designed to replace SCORM in modern LMSs: it keeps the launch flow managed by an LMS (like SCORM), but routes xAPI statements to an LRS for much finer tracking.
Defining the cmi5 format
Unlike SCORM (a simple global API) and "free-form" xAPI (statements sent whenever you like), cmi5 enforces a strict sequence of statements, sent in this order:
- Initialized: when the module launches
- Completed: once the content has been gone through
- Passed / Failed: depending on the score obtained
- Terminated: on close, always last
Characteristics of the cmi5 format
- Requires a cmi5-compatible LMS and LRS: the LMS provides an authentication token to fetch dynamically at launch (unlike "free-form" xAPI, where credentials can be hardcoded)
- Tracking as precise as classic xAPI (detailed score, progress, pass/fail)
- Course structure defined by a cmi5.xml file (the equivalent of SCORM's imsmanifest.xml)
- ⚠️ Cannot be tested outside a cmi5-compatible LMS/LRS: session identifiers are provided dynamically at launch, so they can't be hardcoded like with standalone xAPI
- ⚠️ As with xAPI, statements are sent over direct HTTP: the LRS must allow CORS from your export's origin
Example resource
Block placement in the graph follows the same logic as the SCORM tutorial: reuse the SCORM example graph (https://creator.celestory.io/project/phHUew7e6) and replace the SCORM blocks with the cmi5 blocks below, at the same spots (start, intermediate steps, end).
Important note: Only the blocks starting with cmi5 need to be added wherever you like in your graph.
Step 1: Add the Javascript blocks
Block 1: cmi5 - Retrieve launch parameters
function cmi5_getParam(name) {
const url = new URL(window.location.href);
return url.searchParams.get(name);
}
window.CMI5 = {
endpoint: cmi5_getParam("endpoint"),
fetchUrl: cmi5_getParam("fetch"),
actor: JSON.parse(cmi5_getParam("actor")),
registration: cmi5_getParam("registration"),
activityId: cmi5_getParam("activityId"),
authToken: null
};
if (window.CMI5.endpoint && !window.CMI5.endpoint.endsWith("/")) {
window.CMI5.endpoint += "/";
}
These parameters (endpoint, fetch, actor, registration, activityId) are automatically appended to your module's URL by the LMS at launch time: nothing to hardcode here.
Block 2: cmi5 - Send function + token fetch + Initialize
function cmi5_sendStatement(verbId, verbDisplay, resultObj) {
const category = [{ id: "https://w3id.org/xapi/cmi5/context/categories/cmi5" }];
const moveOnVerbs = [
"http://adlnet.gov/expapi/verbs/completed",
"http://adlnet.gov/expapi/verbs/passed",
"http://adlnet.gov/expapi/verbs/failed"
];
if (moveOnVerbs.includes(verbId)) {
category.push({ id: "https://w3id.org/xapi/cmi5/context/categories/moveOn" });
}
const statement = {
actor: window.CMI5.actor,
verb: {
id: verbId,
display: { "en-US": verbDisplay }
},
object: {
id: window.CMI5.activityId,
objectType: "Activity"
},
context: {
registration: window.CMI5.registration,
contextActivities: { category: category }
}
};
if (resultObj) {
statement.result = resultObj;
}
fetch(window.CMI5.endpoint + "statements", {
method: "POST",
headers: {
"Authorization": window.CMI5.authToken,
"Content-Type": "application/json",
"X-Experience-API-Version": "1.0.3"
},
body: JSON.stringify(statement)
}).then(() => console.log("cmi5: statement sent -", verbDisplay))
.catch(error => console.log("cmi5 send error", error));
}
fetch(window.CMI5.fetchUrl, { method: "POST" })
.then(response => response.json())
.then(data => {
window.CMI5.authToken = "Basic " + data["auth-token"];
cmi5_sendStatement("http://adlnet.gov/expapi/verbs/initialized", "initialized");
})
.catch(error => console.log("cmi5 token fetch error", error));
The token retrieved via fetch is valid for the whole session: it's what authenticates every statement that follows.
Block 3: cmi5 - Step
const step = 70;
cmi5_sendStatement(
"http://adlnet.gov/expapi/verbs/progressed",
"progressed",
{
extensions: {
"https://w3id.org/xapi/cmi5/result/extensions/progress": step
}
}
);
You can add as many steps as you like by duplicating this block with different percentages, just like the SCORM steps.
Block 4: cmi5 - Completed + Passed
const scorePercent = 100;
cmi5_sendStatement("http://adlnet.gov/expapi/verbs/completed", "completed", {
completion: true,
duration: "PT0H5M0S"
});
cmi5_sendStatement("http://adlnet.gov/expapi/verbs/passed", "passed", {
score: {
scaled: scorePercent / 100,
raw: scorePercent,
min: 0,
max: 100
},
success: true
});
duration follows the ISO 8601 format (e.g. PT0H5M0S = 5 minutes) — adjust or compute it dynamically based on the actual time spent. On failure, send the failed verb (http://adlnet.gov/expapi/verbs/failed) with success: false instead of passed.
Block 5: cmi5 - Finish Session (always last)
cmi5_sendStatement("http://adlnet.gov/expapi/verbs/terminated", "terminated");
console.log("cmi5: session ended");
The Terminated statement must always be the last one sent: no other statement should go out after this one.
Step 2: Export the project
Export your project as Web or PWA.
Step 3: Create the folder structure
Create a folder titled with your module's name (e.g. module1) and move all the exported content into it.
Step 4: Add the cmi5.xml file
At the root of your package (next to the module1 folder), add a cmi5.xml file describing the course structure:
<?xml version="1.0" encoding="UTF-8"?>
<courseStructure xmlns="https://w3id.org/xapi/profiles/cmi5/v1/CourseStructure.xsd">
<course id="https://yourdomain.com/courses/course-name">
<title><langstring lang="en-US">Course name</langstring></title>
<description><langstring lang="en-US">Course description</langstring></description>
</course>
<au id="https://yourdomain.com/courses/course-name/module1" moveOn="CompletedOrPassed">
<title><langstring lang="en-US">Module name</langstring></title>
<description><langstring lang="en-US">Module description</langstring></description>
<url>module1/index.html</url>
</au>
</courseStructure>
The <au>'s id must match the activityId the LMS will pass at launch: that's what links the received statements back to the right module. moveOn sets the completion condition (Completed, Passed, CompletedAndPassed, CompletedOrPassed, or NotApplicable).
Step 5: Compress into a ZIP
Compress the parent folder (containing cmi5.xml and the module1 folder) into a zip file.
Step 6: Test the cmi5 file
Import the zip into an LMS or a cmi5-compatible tester, for example https://app.cloud.scorm.com/sc/user/Home (SCORM Cloud also handles cmi5). Launch the module then check in the LRS that the statements arrive in order: Initialized, Completed, Passed (or Failed), Terminated.
Updated on: 06/09/2026
Thank you!
