Skip to content

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:

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
// 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.

index.html
<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:

imsmanifest.xml
<?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 identifier to something unique and both title lines 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.2 and the two xmlns addresses 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)

  1. 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 Good to know card: Alder does not recognize the authoring tool, and gives general advice, not steps for your authoring tool.
    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).

  2. 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.”

    Alder's call log: the course page index.html loaded, then LMSInitialize returned true, shown as 'The course started its session with the LMS'.
    A hand-built course that found the connection.
  3. 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.

What an LMS needs from an AI-built courseThe zip you upload holds imsmanifest.xml at the top, the launch page index.html, the script scorm.js and the course's images. Three things make it a course an LMS can track: the manifest at the top of the zip, naming the launch file and the SCORM version; a script that finds the LMS connection, starts, reports and finishes; and every file the course uses inside the zip, with nothing loaded from other sites.THE ZIP YOU UPLOADimsmanifest.xmlindex.htmlscorm.jsimages/ …WHAT MAKES IT TRACK1The manifest at the top of the zipNames the launch file and theSCORM version.2A script that talks to the LMSFinds the connection, starts once,reports, and finishes last.3Every file inside the zipNothing loaded from other sites.
The zip an LMS can track, and the three things that make it work.

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?