Turn an AI-built HTML course into a SCORM package that tracks (Claude, ChatGPT, Cursor)
Updated 26 October 2026
Quick answer
Claude, ChatGPT, Claude Design and coding agents such as Claude Code, Codex or Cursor give you a web page. An LMS needs a SCORM package: a zip with a manifest at the top and a script that reports to the LMS. Four steps turn one into the other:
- Put every file the course uses in one folder, with nothing loaded from other sites.
- Add a small script that talks to the LMS (copy it from this page).
- Add an imsmanifest.xml that names the launch page and SCORM 1.2.
- Zip from inside the folder, not the folder itself.
Then drop the zip into Alder (free, nothing is uploaded). It checks the zip and the manifest, and its test player shows every call the course makes.
Before you start: SCORM 1.2, unless your LMS asks for 2004
SCORM is the standard most LMSs use to launch a course and record its result. It has two versions in use, 1.2 and 2004, with different names for the same things. 1.2 is the one LMSs most widely accept (some, such as TalentLMS, take only 1.2), so this page uses it. If your LMS needs 2004, the names to change are in the guide to AI-built courses that record nothing.
1. Put the whole course in one folder
Make a folder with the course's first page (call it index.html) and everything it uses: scripts, styles, images, video. Links between them should be relative (images/chart.png, not C:\Users\… or a full web address).
- Scripts and fonts from other sites. AI tools often load a styling library, a framework or web fonts from another site. Some LMSs, company networks and offline players block those, and the course then looks broken or blank. Download them into the folder, or ask the AI tool for a page that needs no outside files.
- One exported HTML file (for example a Claude Design export) is fine: it is the folder's
index.html. - Progress saved in the browser. If the AI tool keeps the learner's place in
localStorage, it stays on that one computer. Keep it in the LMS instead (step 2 and how to move it).
2. Add the script that talks to the LMS
When an LMS opens a SCORM course, it puts a connection object, called the API, on the page around the course. The course has to find it, start a session, report, and finish. Save this as scorm.js in the folder:
// scorm.js: the course's connection to the LMS (SCORM 1.2).
var scorm = (function () {
var api = null;
var finished = false;
var status = '';
var startedAt = Date.now();
// The LMS puts an object called API on one of the windows above the course.
function find(win) {
for (var i = 0; win && i < 10; i++) {
try {
if (win.API) return win.API;
} catch (e) {
return null; // a window on another web address: the search stops here
}
if (win.parent === win) break;
win = win.parent;
}
return null;
}
function time() {
var s = Math.round((Date.now() - startedAt) / 1000);
var two = function (n) { return (n < 10 ? '0' : '') + n; };
return two(Math.floor(s / 3600)) + ':' + two(Math.floor(s / 60) % 60) + ':' + two(s % 60);
}
return {
// Call once, when the course opens.
start: function () {
api = find(window) || (window.opener && find(window.opener));
if (!api) {
console.warn('No LMS found: nothing will be recorded.');
return false;
}
api.LMSInitialize('');
status = api.LMSGetValue('cmi.core.lesson_status');
if (status === 'not attempted') this.set('cmi.core.lesson_status', 'incomplete');
return true;
},
get: function (name) {
return api && !finished ? api.LMSGetValue(name) : '';
},
set: function (name, value) {
if (!api || finished) return;
if (name === 'cmi.core.lesson_status') status = value;
if (api.LMSSetValue(name, String(value)) !== 'true') {
console.error('The LMS refused ' + name + ' = ' + value + ' (error ' + api.LMSGetLastError() + ')');
}
},
// Save now, so progress survives a closed tab.
save: function () {
if (api && !finished) api.LMSCommit('');
},
// Call when the learner leaves. Also runs when the window closes.
finish: function () {
if (!api || finished) return;
var done = status === 'completed' || status === 'passed' || status === 'failed';
this.set('cmi.core.exit', done ? '' : 'suspend');
this.set('cmi.core.session_time', time());
api.LMSCommit('');
api.LMSFinish('');
finished = true;
},
};
})();
window.addEventListener('pagehide', function () {
scorm.finish();
});Then load it on the first page, start it once, and report when the learner is done. If the course has a quiz, send the score too: how to send a score and pass or fail.
<script src="scorm.js"></script>
<script>
scorm.start();
</script>
<!-- When the learner reaches the end: -->
<script>
scorm.set('cmi.core.lesson_status', 'completed');
scorm.save();
</script>If an AI tool is writing the course, give it scorm.js and ask it to call scorm.set and scorm.save rather than write its own LMS code. For a coding agent, put that rule in its instructions file: SCORM rules for Claude Code, Codex and Cursor.
3. Add imsmanifest.xml
The manifest is the file the LMS reads first: it says which page to launch and which SCORM version the course speaks. Save this as imsmanifest.xml in the folder, next to index.html:
<?xml version="1.0" encoding="UTF-8"?>
<manifest identifier="com.example.my-course" version="1"
xmlns="http://www.imsproject.org/xsd/imscp_rootv1p1p2"
xmlns:adlcp="http://www.adlnet.org/xsd/adlcp_rootv1p2">
<metadata>
<schema>ADL SCORM</schema>
<schemaversion>1.2</schemaversion>
</metadata>
<organizations default="org">
<organization identifier="org">
<title>My course</title>
<item identifier="item1" identifierref="res1">
<title>My course</title>
</item>
</organization>
</organizations>
<resources>
<resource identifier="res1" type="webcontent" adlcp:scormtype="sco" href="index.html">
<file href="index.html"/>
<file href="scorm.js"/>
</resource>
</resources>
</manifest>- Change the
identifierto something unique and bothtitlelines to the course name. The LMS shows the title. href="index.html"is the launch page. If yours has another name, change it here.- List the course's files as
<file>lines. Alder checks that every file listed is in the zip. - Keep
1.2and the twoxmlnsaddresses as they are. The version in the manifest has to match the script: a 2004 manifest over this 1.2 script means the course never connects.
4. Zip from inside the folder
Open the folder, select everything in it, and zip those items. On a Mac, select all and choose Compress; on Windows, select all and choose Send to → Compressed (zipped) folder. Or, from a terminal in the folder: zip -r ../my-course.zip .. Zipping the folder itself puts the manifest one level down, and LMSs reject the upload (“imsmanifest.xml not found”).
Check the zip before you upload it (free)
Drop the zip into Alder
Open Alder and drop the zip. It is read in your browser; nothing is uploaded. Alder won't recognize an AI tool as an authoring tool and says so under “things good to know”. That is expected: it still runs the general checks (the zip layout, the manifest, and the test player), and only checks on an authoring tool's own settings are skipped.

Alder's SCORM 1.2 probe, a hand-built package like an AI-built one. Problems show as cards with the fix: a manifest inside an extra folder (STRUCT-001), a launch file that isn't in the zip (STRUCT-003), listed files that are missing (STRUCT-004), and a manifest whose SCORM version doesn't match the script (MANIFEST-012).
Launch it in the test player
Click “Launch the course”, then click through it as a learner. Alder plays the LMS and lists every call. Open the technical log: one of the first rows should say “The course started its session with the LMS.”

A hand-built course that found the connection. Press “I’m done”
Alder says what an LMS would record and lists what went wrong in the session: a course that never connected, never finished, asked for files that aren't in the zip, tried to reach other sites, or saved progress in the browser. Each one is explained in the guide to AI-built courses that record nothing.
Why an AI-built page doesn't track on its own
An LMS can't launch a bare web page as a course. It reads the manifest to find the page, opens it with the API around it, and then waits for the page to report. A page that never calls the API looks, to the LMS, like a course nobody finished: no completion, no score, no time. That is why the three pieces below are all needed.
Free wrappers can do steps 2 to 4 for you when the course is one HTML file. They add their own manifest and script. Check their zip the same way: the problems are the same ones.
Still not working?
- It uploads but records nothing, or only some of it: see AI-built course plays but records nothing.
- You want the check to run every time the AI tool changes the course: see test a SCORM package automatically with Playwright.
- The course shows a blank page in the LMS: see blank screen or endless loading.