Cutting SCORM packaging from 4 hours to 10 minutes
Role: Senior Instructional Designer & SCORM Team Lead, Kidvento Team: 8 (4 developers, 4 interns) Output affected: 100+ K-12 SCORM packages
The problem
After every Storyline publish, the team ran a manual checklist before a module could ship:
- Unzip the published package.
- Open
imsmanifest.xml, verify the SCO metadata and mastery score. - Inject the LMS-specific reporting hook into the launch HTML.
- Patch the completion-status flag — Storyline marks
incompleteon close even after a passed assessment. - Sanity-check that every interaction has a unique
cmi.interactions.N.id. - Re-zip with the right directory structure.
- Smoke-test in the staging LMS.
- Hand off.
On a clean module this took roughly 4 hours per package. On a module that came back from QA, the four hours started over.
The change
I built a Node CLI that automates steps 2–6 and replaces step 7 with a deterministic check. Per-module effort dropped to about 10 minutes.
What the script actually does:
- Manifest patcher. Reads
imsmanifest.xml, applies a per-LMS overlay, writes it back. Normalises theadlcpnamespace declaration onto the root element — Storyline occasionally emits it on a child element, which some LMSs reject. - HTML hook injector. Inserts the LMS reporting hook before
</body>, after the SCORM API wrapper is loaded. Script load order matters here. - Completion override. Patches the lesson-status logic so a passed assessment forces
passedrather than letting an unload handler overwrite it withincomplete. - Interaction ID auditor. Walks the SCORM Player's
cmi.interactions.*calls and asserts uniqueness. Fails the build on collision. - Deterministic re-zip.
zip -X --sort-nameso two identical builds produce byte-identical zips. Makes QA diffs trivial. - Smoke test. Opens the package in headless Chromium against a local SCORM RTE emulator, walks the slides, asserts that
lesson_statusfires, score lands, no console errors, all interactions logged.
What I learned
The 4-hour packaging step had been hiding bugs we hadn't noticed.
Three findings drove the rewrite:
- **The
lesson_statusrace.** Storyline writesincompleteonbeforeunloadeven after a passed assessment. Our LMS read the last write and ignored the earlierpassed. The override has to be set after Storyline's own unload handler — load order matters. - Manifest namespace drift. Storyline 360 occasionally emits manifests where the
adlcpnamespace declaration sits on a child element. Most LMSs forgive this; two of our targets did not. The patcher normalises it at the root regardless. - Silent interaction-ID collisions. Storyline's auto-numbered interaction IDs reset between scenes. A copy-pasted slide across scenes produces two interactions with the same id. The LMS records the last one and discards the first. Worth catching at build time, because the data loss is otherwise invisible.
The auditor catches all three now. Two of them were not on my radar before I started building the pipeline. The interesting outcome of automating a tedious manual step is that you often find the bugs the manual step was masking.
Impact
- Per-package effort: 4 hours → 10 minutes on the clean path.
- Build determinism: byte-identical re-runs make QA diffs a
diff -rinstead of a side-by-side review. - Developer focus: the four hours per package previously spent on packaging now flow into authoring, voiceover review, and accessibility audits.
Stack
JavaScript (Node), shell, headless Chromium via Puppeteer, XML parsing, the SCORM 1.2 RTE spec.
Ownership
The script is internal to Kidvento. The findings — lesson_status race, namespace drift, interaction-ID collisions — generalise across Storyline 360 workflows, which is why this writeup exists. The open-source equivalent of the auditor is scorm-lint, in the same portfolio.