Skip to main content

LMS Integration Guide

This guide provides a comprehensive, language-agnostic reference for integrating the scorm-again library into a Learning Management System. It covers all launch scenarios, data requirements, storage considerations, and runtime responsibilities.

Assumptions:

  • Your LMS can parse SCORM content packages (imsmanifest.xml)
  • Your LMS can serve SCORM content to learners via iframe or popup
  • Your LMS has a mechanism for persisting learner data

Table of Contents

  1. Architecture Overview
  2. Data Model Requirements
  3. SCORM 1.2 Integration
  4. SCORM 2004 Integration
  5. Commit Endpoint Specification
  6. Entry Mode Decision Logic
  7. Error Handling
  8. Session Management
  9. Integration Checklists

Architecture Overview

┌─────────────────────────────────────────────────────────────────────────────┐
│ YOUR LMS │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ Package Parser │ │ Launch Manager │ │ Commit Handler │ │
│ │ (Import Phase) │ │ (Runtime) │ │ (Endpoint) │ │
│ └────────┬─────────┘ └────────┬─────────┘ └──────────┬───────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Data Storage Layer │ │
│ │ (Course Structure, Learner State, Runtime Data, Sequencing) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

│ HTTP Commit / postMessage

┌─────────────────────────────────────────────────────────────────────────────┐
│ scorm-again Library │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ API Instance │ │ CMI Data Model │ │ Sequencing Engine │ │
│ │ (Scorm12API or │ │ (Runtime State) │ │ (SCORM 2004 only) │ │
│ │ Scorm2004API) │ │ │ │ │ │
│ └──────────────────┘ └──────────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘

│ JavaScript API calls

┌─────────────────────────────────────────────────────────────────────────────┐
│ SCORM Content (SCO) │
│ Runs in iframe, calls API.LMSInitialize(), SetValue(), etc. │
└─────────────────────────────────────────────────────────────────────────────┘

Key Responsibilities

ComponentResponsibility
Package ParserExtract course structure, sequencing rules, SCO metadata from manifest
Launch ManagerInitialize API with correct data, manage SCO lifecycle
Commit HandlerReceive runtime data, persist to storage, return success/failure
scorm-againProvide SCORM-compliant API, manage runtime state, handle sequencing
SCO ContentCall API methods, report progress/scores/completion

Data Model Requirements

This section defines the data your LMS must store. The structure is storage-agnostic—adapt to your database technology (relational, document, key-value, etc.).

Core Entities

1. Course

Represents an imported SCORM package.

Course
├── id: string (unique identifier)
├── title: string
├── version: string (SCORM version: "1.2" | "2004")
├── scormEdition: string (for 2004: "2nd" | "3rd" | "4th")
├── importedAt: timestamp
├── packageHash: string (for duplicate detection)

├── structure: CourseStructure (see below)
└── metadata: CourseMetadata (optional publisher info, etc.)

2. Course Structure

Defines the organization and items within a course.

CourseStructure
├── organizationId: string (from manifest <organization identifier="">)
├── organizationTitle: string
├── items: [CourseItem] (ordered list)

└── sequencingCollection: [SequencingDefinition] (SCORM 2004 only, for reusable rules)

3. Course Item (SCO or Asset)

Each launchable or structural item in the course.

CourseItem
├── id: string (from <item identifier="">)
├── title: string
├── resourceIdentifier: string (from <item identifierref="">)
├── launchUrl: string (resolved from resource href)
├── scormType: "sco" | "asset" (SCOs are trackable, assets are not)
├── sequenceIndex: integer (order within parent)
├── parentItemId: string | null (for hierarchical structures)
├── isVisible: boolean (default true)

├── parameters: string (query parameters for launch URL)
├── prerequisites: string (SCORM 1.2 only, AICC script syntax)

│ // Read-only data from manifest (provided to SCO, not modifiable)
├── masteryScore: number | null (0-100 for 1.2, -1 to 1 scaled for 2004)
├── maxTimeAllowed: string | null (time format varies by version)
├── timeLimitAction: string | null ("exit,message", etc.)
├── dataFromLms: string | null (launch_data content)
├── completionThreshold: number | null (SCORM 2004 only, 0.0-1.0)
├── scaledPassingScore: number | null (SCORM 2004 only, -1.0 to 1.0)

│ // Sequencing (SCORM 2004 only)
├── sequencingRules: SequencingRules | null
├── sequencingControls: SequencingControls | null
├── rollupRules: RollupRules | null
├── objectives: [ObjectiveDefinition] | null
├── deliveryControls: DeliveryControls | null

└── children: [CourseItem] (for nested items)

4. Learner

Basic learner information.

Learner
├── id: string (unique identifier in your system)
├── displayName: string (format for SCORM: "Last, First" for 1.2, any format for 2004)
├── email: string | null
└── externalId: string | null (if synced from external system)

5. Course Registration

Links a learner to a course (enrollment).

CourseRegistration
├── id: string (unique identifier)
├── courseId: string (references Course)
├── learnerId: string (references Learner)
├── registeredAt: timestamp
├── expiresAt: timestamp | null

├── currentAttempt: integer (1-based, increments on new attempt)
├── currentScoId: string | null (last/current SCO)

│ // Course-level rollup (calculated from SCO states)
├── overallStatus: string ("not_started" | "in_progress" | "completed" | "passed" | "failed")
├── overallScore: number | null
├── totalTimeSeconds: number

├── startedAt: timestamp | null
├── completedAt: timestamp | null
└── lastAccessedAt: timestamp | null

6. Attempt

Represents a single attempt at a course (may contain multiple SCO attempts).

Attempt
├── id: string (unique identifier)
├── registrationId: string (references CourseRegistration)
├── attemptNumber: integer (1, 2, 3, ...)
├── startedAt: timestamp
├── completedAt: timestamp | null
├── status: string ("active" | "suspended" | "completed" | "abandoned")

│ // Sequencing state (SCORM 2004 only)
├── sequencingState: SequencingState | null
└── globalObjectives: GlobalObjectiveState | null

7. SCO Attempt State

The runtime data for a single SCO within an attempt.

ScoAttemptState
├── id: string (unique identifier)
├── attemptId: string (references Attempt)
├── scoId: string (references CourseItem)
├── attemptNumber: integer (attempt at this specific SCO)

│ // Core tracking data
├── completionStatus: string
├── successStatus: string
├── lessonStatus: string (SCORM 1.2 only, combined field)
├── location: string | null (bookmark, max 255 chars for 1.2, 1000 for 2004)
├── suspendData: string | null (max 4096 chars for 1.2, 64000 for 2004)

│ // Score data
├── scoreRaw: number | null
├── scoreMin: number | null
├── scoreMax: number | null
├── scoreScaled: number | null (SCORM 2004 only, -1.0 to 1.0)

│ // Progress (SCORM 2004 only)
├── progressMeasure: number | null (0.0 to 1.0)

│ // Time tracking
├── totalTimeSeconds: number (accumulated across sessions)
├── sessionTimeSeconds: number (current/last session)

│ // Entry tracking
├── entryMode: string ("ab-initio" | "resume" | "")
├── exitMode: string ("" | "suspend" | "logout" | "normal" | "timeout" for 2004)

│ // Detailed runtime data (store as structured data or JSON)
├── objectives: [ObjectiveState]
├── interactions: [InteractionState]
├── comments: [CommentState] | null (SCORM 2004)
├── learnerPreferences: LearnerPreferenceState

│ // Session tracking
├── firstLaunchedAt: timestamp | null
├── lastLaunchedAt: timestamp | null
├── lastCommitAt: timestamp | null
├── sessionCount: integer

│ // Full CMI snapshot (optional, for debugging/audit)
└── rawCmiData: object | null

8. Objective State

Per-objective tracking data.

ObjectiveState
├── id: string (objective identifier, required for SCORM 2004)
├── index: integer (position in objectives array)

│ // SCORM 1.2 fields
├── score_raw: number | null
├── score_min: number | null
├── score_max: number | null
├── status: string | null ("passed", "completed", "failed", etc.)

│ // SCORM 2004 fields
├── success_status: string | null ("passed", "failed", "unknown")
├── completion_status: string | null ("completed", "incomplete", "not attempted", "unknown")
├── progress_measure: number | null (0.0 to 1.0)
├── score_scaled: number | null (-1.0 to 1.0)
├── description: string | null (localizable)

└── isGlobal: boolean (true if this is a shared global objective)

9. Interaction State

Question/interaction response data.

InteractionState
├── id: string (interaction identifier)
├── index: integer (position in interactions array)
├── type: string ("true-false", "choice", "fill-in", "long-fill-in", "matching", "performance", "sequencing", "likert", "numeric", "other")
├── timestamp: string (ISO 8601 for 2004, CMITime for 1.2)
├── weighting: number | null
├── learner_response: string (encoded per type)
├── result: string ("correct", "incorrect", "unanticipated", "neutral", or numeric)
├── latency: string (time format)
├── description: string | null (SCORM 2004)
├── correct_responses: [CorrectResponsePattern]
└── objectives: [string] (objective IDs this interaction relates to)

10. Global Objective State (SCORM 2004 Only)

Objectives shared across multiple SCOs.

GlobalObjectiveState
├── attemptId: string (references Attempt)
├── objectiveId: string (global objective identifier from manifest)

├── success_status: string | null
├── completion_status: string | null
├── progress_measure: number | null
├── score_scaled: number | null
├── score_raw: number | null
├── score_min: number | null
└── score_max: number | null

11. Sequencing State (SCORM 2004 Only)

Runtime sequencing state for a course attempt.

SequencingState
├── attemptId: string (references Attempt)
├── currentActivityId: string | null
├── suspendedActivityId: string | null

│ // Per-activity tracking
├── activityStates: [ActivityState]

│ // Serialized state (if using library's built-in serialization)
└── serializedState: string | null
ActivityState
├── activityId: string
├── isActive: boolean
├── isSuspended: boolean
├── attemptCount: integer
├── attemptAbsoluteDuration: number (seconds)
├── attemptExperiencedDuration: number (seconds)

├── completionStatus: string
├── progressMeasure: number | null
├── completionSetByContent: boolean

├── objectiveSatisfied: boolean | null
├── objectiveMeasure: number | null
├── objectiveSetByContent: boolean

└── objectiveStates: [ActivityObjectiveState]

12. Learner Preferences

Per-learner preferences that may persist across SCOs.

LearnerPreferenceState
│ // SCORM 1.2
├── audio: number | null (-1 to 100, -1 = off)
├── language: string | null (ISO language code)
├── speed: number | null (-100 to 100)
├── text: number | null (-1 to 1, -1 = off)

│ // SCORM 2004 (different data types than 1.2)
├── audio_level: number | null (0.0+, default: 1.0)
├── audio_captioning: integer | null (-1, 0, or 1; default: 0)
│ // -1 = no preference, 0 = captioning off, 1 = captioning on
├── delivery_speed: number | null (0.0+, default: 1.0)
└── language: string | null (ISO language code, default: "")

Entity Relationships

Course (1) ─────────────────────────── (N) CourseItem
│ │
│ │ (self-referencing for hierarchy)
│ │
└──── (N) CourseRegistration └── (1) Parent CourseItem


└──── (N) Attempt

├──── (N) ScoAttemptState
│ │
│ ├── (N) ObjectiveState
│ └── (N) InteractionState

├──── (1) SequencingState (SCORM 2004)
│ │
│ └── (N) ActivityState

└──── (N) GlobalObjectiveState (SCORM 2004)

Learner (1) ──── (N) CourseRegistration

SCORM 1.2 Integration

SCORM 1.2 Single-SCO Launches

Data Required at Launch

The LMS must provide the following data before the SCO calls LMSInitialize():

Data ElementCMI PathSourceRequiredNotes
Student IDcmi.core.student_idLearner entityYesUnique learner identifier
Student Namecmi.core.student_nameLearner entityYesFormat: "Last, First"
Lesson Statuscmi.core.lesson_statusPrevious ScoAttemptStateNoEmpty if new attempt
Lesson Locationcmi.core.lesson_locationPrevious ScoAttemptStateNoBookmark, max 255 chars
Lesson Modecmi.core.lesson_modeLMS DecisionYes"normal", "browse", "review"
Creditcmi.core.creditLMS DecisionYes"credit" or "no-credit"
Entrycmi.core.entryLMS DecisionYes"ab-initio" or "resume"
Total Timecmi.core.total_timePrevious ScoAttemptStateNoFormat: HHHH:MM:SS.SS
Score Rawcmi.core.score.rawPrevious ScoAttemptStateNoMust be 0-100 range
Score Mincmi.core.score.minPrevious ScoAttemptStateNoMust be 0-100 range
Score Maxcmi.core.score.maxPrevious ScoAttemptStateNoMust be 0-100 range
Suspend Datacmi.suspend_dataPrevious ScoAttemptStateNoMax 4096 chars
Launch Datacmi.launch_dataCourseItem.dataFromLmsNoRead-only, from manifest
Commentscmi.commentsPrevious ScoAttemptStateNoLearner feedback, max 4096 chars
Comments from LMScmi.comments_from_lmsLMSNoRead-only, max 4096 chars
Mastery Scorecmi.student_data.mastery_scoreCourseItem.masteryScoreNoRead-only, 0-100, from manifest
Max Time Allowedcmi.student_data.max_time_allowedCourseItem.maxTimeAllowedNoRead-only, format: HHHH:MM:SS
Time Limit Actioncmi.student_data.time_limit_actionCourseItem.timeLimitActionNoRead-only, from manifest
Objectivescmi.objectives.n.*Previous ScoAttemptStateNoPrevious objective states
Student Preferencescmi.student_preference.*Previous ScoAttemptStateNoaudio, language, speed, text

Important Notes:

  • Score Normalization: All SCORM 1.2 scores must be normalized to 0-100 range per specification.
  • Exit Mode Values: Valid values for cmi.core.exit are: "" (empty), "suspend", "logout", "time-out".
  • Session Time: cmi.core.session_time is calculated by the library and is write-only. Only total_time should be persisted between sessions.

Launch Implementation

// 1. Create API instance
const api = new Scorm12API({
lmsCommitUrl: "/api/scorm/1.2/commit",
autocommit: true,
autocommitSeconds: 60,

// mastery_override: When true (default), if mastery_score is set and the learner's
// score meets or exceeds it, the API will automatically set lesson_status to "passed".
// If the score is below mastery_score, it will be set to "failed".
// Set to false if you want the SCO to control pass/fail status entirely.
mastery_override: true
});

// 2. Load data before SCO starts
api.loadFromJSON({
cmi: {
core: {
student_id: learner.id,
student_name: formatName(learner), // "Last, First"
lesson_status: previousState?.lessonStatus || "",
lesson_location: previousState?.location || "",
lesson_mode: determineLessonMode(registration, sco),
credit: determineCredit(registration),
entry: previousState?.suspendData ? "resume" : "ab-initio",
total_time: previousState?.totalTime || "0000:00:00.00",
score: {
raw: previousState?.scoreRaw?.toString() || "",
min: previousState?.scoreMin?.toString() || "0",
max: previousState?.scoreMax?.toString() || "100"
}
},
suspend_data: previousState?.suspendData || "",
launch_data: sco.dataFromLms || "",

// Comments - learner feedback (read/write)
comments: previousState?.comments || "",
// Comments from LMS - instructor/system feedback (read-only, set before init)
comments_from_lms: lmsComments || "",

student_data: {
mastery_score: sco.masteryScore?.toString() || "",
max_time_allowed: sco.maxTimeAllowed || "",
time_limit_action: sco.timeLimitAction || ""
},

// Restore objectives if resuming
objectives: previousState?.objectives || {},

// Restore learner preferences
student_preference: previousState?.learnerPreferences || {
audio: "", // -1 to 100 (-1 = off)
language: "", // ISO language code
speed: "", // -100 to 100
text: "" // -1 to 1 (-1 = off)
}
}
});

// 3. Attach to window for SCO discovery
window.API = api;

// 4. Launch SCO in iframe
contentFrame.src = sco.launchUrl;

Handling Commit Data

When your commit endpoint receives data, extract and store:

async function handleScorm12Commit(commitData, registration, scoId) {
const cmi = commitData.runtimeData?.cmi || commitData.cmi;

// Extract core data
const scoState = {
scoId: scoId,
attemptId: registration.currentAttemptId,

// Status
lessonStatus: cmi.core?.lesson_status || "not attempted",

// Location/Bookmark
location: cmi.core?.lesson_location || null,
suspendData: cmi.suspend_data || null,

// Score (must be 0-100 range per SCORM 1.2 spec)
scoreRaw: parseFloat(cmi.core?.score?.raw) || null,
scoreMin: parseFloat(cmi.core?.score?.min) || null,
scoreMax: parseFloat(cmi.core?.score?.max) || null,

// Time (parse HHHH:MM:SS.SS format)
// NOTE: Only persist total_time. session_time is transient and calculated
// by the library - it represents the current session only.
totalTimeSeconds: parseScorm12Time(cmi.core?.total_time),

// Exit mode - determines next launch's entry value
// Valid values: "" (normal), "suspend", "logout", "time-out"
exitMode: cmi.core?.exit || "",

// Comments from learner (max 4096 chars)
comments: cmi.comments || null,

// Objectives
objectives: extractObjectives(cmi.objectives),

// Interactions
interactions: extractInteractions(cmi.interactions),

// Preferences (persist for resume)
learnerPreferences: {
audio: cmi.student_preference?.audio, // -1 to 100
language: cmi.student_preference?.language, // ISO language code
speed: cmi.student_preference?.speed, // -100 to 100
text: cmi.student_preference?.text // -1 to 1
},

// Metadata
lastCommitAt: new Date()
};

await saveScoAttemptState(scoState);

// Update registration rollup
await updateRegistrationStatus(registration, scoState);

return { result: "true" };
}

SCORM 1.2 Multi-SCO Launches

Multi-SCO courses require additional coordination since SCORM 1.2 has no sequencing specification.

Additional Storage Requirements

Per-registration, track:

  • Current SCO ID
  • SCO navigation order (from manifest sequence or custom LMS rules)
  • Per-SCO state (as described above)
  • Course-level rollup calculations

Launch Manager Implementation

class Scorm12MultiScoManager {
constructor(course, registration, learner) {
this.course = course;
this.registration = registration;
this.learner = learner;
this.currentScoId = null;
this.api = null;
}

async launchSco(scoId) {
const sco = this.course.items.find(i => i.id === scoId);
if (!sco || sco.scormType !== 'sco') {
throw new Error(`Invalid SCO: ${scoId}`);
}

// Check prerequisites (LMS-defined logic)
if (!this.checkPrerequisites(sco)) {
throw new Error(`Prerequisites not met for: ${scoId}`);
}

// Get previous state for this SCO
const previousState = await this.getScoState(scoId);

// If switching SCOs, reset the API
if (this.api && this.currentScoId !== scoId) {
this.api.reset();
}

// Create or reconfigure API
if (!this.api) {
this.api = new Scorm12API({
lmsCommitUrl: `/api/scorm/commit/${this.registration.id}/${scoId}`,
autocommit: true,
autocommitSeconds: 60,

// globalStudentPreferences: When true, learner preferences (audio, language,
// speed, text) are stored in memory and shared across all SCOs in the session.
// When a learner changes preferences in one SCO, those preferences are
// automatically available when they navigate to another SCO.
// When false (default), each SCO has isolated preferences.
// For multi-SCO courses, this should typically be true.
globalStudentPreferences: true
});

this.setupEventHandlers();
}

// Load SCO data
this.api.loadFromJSON({
cmi: {
core: {
student_id: this.learner.id,
student_name: formatName(this.learner),
lesson_status: previousState?.lessonStatus || "",
lesson_location: previousState?.location || "",
lesson_mode: "normal",
credit: "credit",
entry: previousState?.suspendData ? "resume" : "ab-initio",
total_time: formatScorm12Time(previousState?.totalTimeSeconds || 0),
score: {
raw: previousState?.scoreRaw?.toString() || "",
min: previousState?.scoreMin?.toString() || "0",
max: previousState?.scoreMax?.toString() || "100"
}
},
suspend_data: previousState?.suspendData || "",
launch_data: sco.dataFromLms || "",
student_data: {
mastery_score: sco.masteryScore?.toString() || "",
max_time_allowed: sco.maxTimeAllowed || "",
time_limit_action: sco.timeLimitAction || ""
}
}
});

// Update tracking
this.currentScoId = scoId;
await this.updateRegistration({ currentScoId: scoId });

// Launch
window.API = this.api;
document.getElementById('content-frame').src = sco.launchUrl;
}

setupEventHandlers() {
// Handle navigation requests from content
this.api.on('SequenceNext', () => {
const nextSco = this.getNextSco(this.currentScoId);
if (nextSco) {
this.launchSco(nextSco.id);
} else {
this.showCourseComplete();
}
});

this.api.on('SequencePrevious', () => {
const prevSco = this.getPreviousSco(this.currentScoId);
if (prevSco) {
this.launchSco(prevSco.id);
}
});

this.api.on('LMSFinish', async () => {
// SCO terminated, update rollup
await this.updateCourseRollup();
});
}

checkPrerequisites(sco) {
// Implement your prerequisite logic
// SCORM 1.2 uses AICC script syntax in <prerequisites>
// Most LMSs implement simpler "previous must be complete" logic
if (!sco.prerequisites) return true;

// Example: simple sequential prerequisite
const prevSco = this.getPreviousSco(sco.id);
if (prevSco) {
const prevState = this.getScoStateSync(prevSco.id);
return ['completed', 'passed'].includes(prevState?.lessonStatus);
}
return true;
}

async updateCourseRollup() {
const allScoStates = await this.getAllScoStates();
const scos = this.course.items.filter(i => i.scormType === 'sco');

// Calculate completion
const completedCount = allScoStates.filter(s =>
['completed', 'passed'].includes(s.lessonStatus)
).length;
const completionPct = (completedCount / scos.length) * 100;

// Calculate average score
const scores = allScoStates
.filter(s => s.scoreRaw !== null)
.map(s => s.scoreRaw);
const avgScore = scores.length > 0
? scores.reduce((a, b) => a + b, 0) / scores.length
: null;

// Calculate total time
const totalTime = allScoStates.reduce(
(sum, s) => sum + (s.totalTimeSeconds || 0), 0
);

// Determine overall status
let overallStatus = 'not_started';
if (allScoStates.some(s => s.lessonStatus)) {
overallStatus = 'in_progress';
}
if (completedCount === scos.length) {
overallStatus = 'completed';
// Check if all passed
const allPassed = allScoStates.every(s =>
s.lessonStatus === 'passed' ||
(s.lessonStatus === 'completed' && s.scoreRaw >= (scos.find(sc => sc.id === s.scoId)?.masteryScore || 0))
);
if (allPassed) {
overallStatus = 'passed';
}
}

await this.updateRegistration({
overallStatus,
overallScore: avgScore,
totalTimeSeconds: totalTime,
completedAt: overallStatus === 'completed' || overallStatus === 'passed'
? new Date() : null
});
}
}

SCORM 2004 Integration

SCORM 2004 Single-SCO Launches

Data Required at Launch

Data ElementCMI PathSourceRequiredNotes
Learner IDcmi.learner_idLearner entityYesUnique identifier
Learner Namecmi.learner_nameLearner entityYesAny format (LangString)
Completion Statuscmi.completion_statusPrevious stateNo"completed", "incomplete", "not attempted", "unknown"
Success Statuscmi.success_statusPrevious stateNo"passed", "failed", "unknown"
Locationcmi.locationPrevious stateNoBookmark, max 1000 chars
Suspend Datacmi.suspend_dataPrevious stateNoMax 64000 chars
Total Timecmi.total_timePrevious stateNoISO 8601 Duration (PTnHnMnS)
Modecmi.modeLMS DecisionYes"normal", "browse", "review"
Creditcmi.creditLMS DecisionYes"credit", "no-credit"
Entrycmi.entryLMS DecisionYes"ab-initio", "resume", ""
Launch Datacmi.launch_dataCourseItemNoRead-only, from manifest
Max Time Allowedcmi.max_time_allowedCourseItemNoRead-only, ISO 8601
Time Limit Actioncmi.time_limit_actionCourseItemNoRead-only
Completion Thresholdcmi.completion_thresholdCourseItemNoRead-only, 0.0-1.0
Scaled Passing Scorecmi.scaled_passing_scoreCourseItemNoRead-only, -1.0 to 1.0
Progress Measurecmi.progress_measurePrevious stateNo0.0-1.0
Score (all fields)cmi.score.*Previous stateNoscaled, raw, min, max
Objectivescmi.objectives.n.*Previous stateNoAll objective data
Comments from LMScmi.comments_from_lms.n.*LMSNoRead-only

Auto-Evaluation of Status Fields

Important: SCORM 2004 implements automatic evaluation of completion_status and success_status based on thresholds set from the manifest. This is a critical behavior to understand:

Completion Status Auto-Evaluation: When cmi.completion_threshold is set (from manifest <adlcp:completionThreshold>):

  • If cmi.progress_measure >= completion_threshold → returns "completed"
  • If cmi.progress_measure < completion_threshold → returns "incomplete"
  • If progress_measure is not set → returns "unknown"

Success Status Auto-Evaluation: When cmi.scaled_passing_score is set (from manifest <imsss:minNormalizedMeasure>):

  • If cmi.score.scaled >= scaled_passing_score → returns "passed"
  • If cmi.score.scaled < scaled_passing_score → returns "failed"
  • If score.scaled is not set → returns "unknown"

Implication for LMS: The values returned by GetValue("cmi.completion_status") and GetValue("cmi.success_status") may be dynamically computed, not the stored values. Your commit handler will receive these auto-evaluated values.

Launch Implementation

const api = new Scorm2004API({
lmsCommitUrl: "/api/scorm/2004/commit",
autocommit: true,
autocommitSeconds: 60
});

api.loadFromJSON({
cmi: {
learner_id: learner.id,
learner_name: learner.displayName,

// Status (may be auto-evaluated if thresholds are set)
completion_status: previousState?.completionStatus || "unknown",
success_status: previousState?.successStatus || "unknown",

// Location and suspend data
location: previousState?.location || "",
suspend_data: previousState?.suspendData || "",

// Time (ISO 8601 Duration format)
total_time: previousState?.totalTime || "PT0S",

// Mode and credit
mode: determineLessonMode(registration, sco),
credit: determineCredit(registration),
// entry: "" (empty) should be used for "review" mode
entry: determineEntry(previousState, sco.mode),

// Manifest data (read-only, triggers auto-evaluation if set)
launch_data: sco.dataFromLms || "",
max_time_allowed: sco.maxTimeAllowed || "",
time_limit_action: sco.timeLimitAction || "",
completion_threshold: sco.completionThreshold?.toString() || "",
scaled_passing_score: sco.scaledPassingScore?.toString() || "",

// Progress and score
progress_measure: previousState?.progressMeasure?.toString() || "",
score: {
scaled: previousState?.scoreScaled?.toString() || "",
raw: previousState?.scoreRaw?.toString() || "",
min: previousState?.scoreMin?.toString() || "",
max: previousState?.scoreMax?.toString() || ""
},

// Complex data
objectives: previousState?.objectives || {},
interactions: previousState?.interactions || {},

// Comments from LMS - array of {comment, location, timestamp}
// Each comment has: comment (string), location (string, max 250 chars),
// timestamp (ISO 8601 format). These are read-only after initialization.
comments_from_lms: lmsComments || {},

// Comments from learner - can be set by SCO
comments_from_learner: previousState?.commentsFromLearner || {},

// Learner preferences with correct default values
learner_preference: previousState?.learnerPreferences || {
audio_level: "1", // 0.0+ (default: 1.0)
audio_captioning: "0", // -1, 0, or 1 (default: 0 = off)
delivery_speed: "1", // 0.0+ (default: 1.0)
language: "" // ISO language code (default: "")
}
}
});

window.API_1484_11 = api;
contentFrame.src = sco.launchUrl;

Objective ID Immutability

Important: In SCORM 2004, once cmi.objectives.n.id is set, it cannot be changed. Attempting to change an objective ID will result in error code 351 (GENERAL_SET_FAILURE). Objective IDs must be set before other objective properties. This is per SCORM 2004 specification.


SCORM 2004 Multi-SCO Launches

Similar to single-SCO but with:

  • API reset between SCO transitions
  • State management per SCO
  • Course-level rollup

This is appropriate when the course has multiple SCOs but does NOT use SCORM 2004 sequencing.


SCORM 2004 Sequenced Modules

Sequenced courses use the SCORM 2004 Sequencing and Navigation specification. The library handles sequencing logic internally.

Additional Manifest Data Required

Parse from <imsss:sequencing> elements:

// Activity tree structure
const activityTree = {
id: "root",
title: organization.title,
children: parseItems(organization.items),
sequencingControls: parseControlMode(organization),
sequencingRules: parseSequencingRules(organization),
rollupRules: parseRollupRules(organization),
objectives: parseObjectives(organization)
};

// Helper to parse items recursively
function parseItems(items) {
return items.map(item => ({
id: item.identifier,
title: item.title,
isVisible: item.isvisible !== "false",
children: item.children ? parseItems(item.children) : [],

// Sequencing data
sequencingControls: parseControlMode(item),
sequencingRules: parseSequencingRules(item),
rollupRules: parseRollupRules(item),
objectives: parseObjectives(item),

// Time limits
attemptAbsoluteDurationLimit: item.attemptAbsoluteDurationLimit,
attemptExperiencedDurationLimit: item.attemptExperiencedDurationLimit,
beginTimeLimit: item.beginTimeLimit,
endTimeLimit: item.endTimeLimit,

// Delivery controls
deliveryControls: {
completionSetByContent: item.completionSetByContent !== "false",
objectiveSetByContent: item.objectiveSetByContent !== "false"
}
}));
}

Sequencing Configuration

const api = new Scorm2004API({
lmsCommitUrl: "/api/scorm/2004/commit",

// Sequencing configuration
sequencing: {
activityTree: activityTree,

// Optional: provide time functions
now: () => new Date(),
getAttemptElapsedSeconds: (activity) => {
return getElapsedSecondsForActivity(activity.id);
},

// Event listeners for LMS coordination
eventListeners: {
// Called when a new activity should be delivered
onActivityDelivery: (activity) => {
console.log(`Deliver activity: ${activity.id}`);
launchActivityContent(activity);
},

// Called when navigation validity changes (for UI updates)
onNavigationValidityUpdate: (validity) => {
updateNavigationUI({
canContinue: validity.continue,
canPrevious: validity.previous,
choiceTargets: validity.choice,
jumpTargets: validity.jump
});
},

// Called when rollup completes
onRollupComplete: (rootActivity) => {
console.log('Rollup complete, root status:', rootActivity.completionStatus);
}
}
},

// Valid SCO IDs for navigation validation
scoItemIds: extractScoIds(activityTree),

// Or provide custom validator
scoItemIdValidator: (scoId) => {
return database.validateScoId(scoId);
},

// Global objectives that persist across SCOs
globalObjectiveIds: extractGlobalObjectiveIds(activityTree)
});

Global Objectives

Global objectives require special handling:

Identifying Global Objectives (from manifest):

<imsss:objectives>
<imsss:primaryObjective objectiveID="primary-obj" satisfiedByMeasure="true">
<imsss:mapInfo targetObjectiveID="global-objective-1" readSatisfiedStatus="true" writeSatisfiedStatus="true"/>
</imsss:primaryObjective>
</imsss:objectives>

Storing Global Objectives:

// When handling commit data
async function handleSequencedCommit(commitData, registration) {
const cmi = commitData.runtimeData?.cmi;
const globalObjectiveIds = registration.globalObjectiveIds;

// Save SCO-specific data
await saveScoAttemptState(commitData.scoId, cmi);

// Extract and save global objectives separately
if (cmi.objectives) {
for (const [index, obj] of Object.entries(cmi.objectives)) {
if (globalObjectiveIds.includes(obj.id)) {
await saveGlobalObjective(registration.attemptId, obj);
}
}
}
}

// When launching a SCO, merge global objectives
async function prepareObjectivesForLaunch(scoId, attemptId, globalObjectiveIds) {
const localObjectives = await getScoObjectives(scoId, attemptId);
const globalObjectives = await getGlobalObjectives(attemptId);

// Merge global objectives into local array based on mapInfo
const merged = { ...localObjectives };
let nextIndex = Object.keys(merged).length;

for (const globalObj of globalObjectives) {
// Check if this SCO references this global objective
if (scoReferencesGlobalObjective(scoId, globalObj.id)) {
// Find or create slot
const existingIndex = Object.entries(merged)
.find(([_, obj]) => obj.id === globalObj.id)?.[0];

if (existingIndex !== undefined) {
merged[existingIndex] = { ...merged[existingIndex], ...globalObj };
} else {
merged[nextIndex++] = globalObj;
}
}
}

return merged;
}

Sequencing State Persistence

// Configure state persistence callbacks
const api = new Scorm2004API({
// ... other settings ...

sequencingStatePersistence: {
persistence: {
saveState: async (stateData, metadata) => {
await database.saveSequencingState({
learnerId: metadata.learnerId,
courseId: metadata.courseId,
attemptNumber: metadata.attemptNumber,
stateData: stateData, // Serialized string
updatedAt: new Date()
});
return true;
},

loadState: async (metadata) => {
const record = await database.getSequencingState({
learnerId: metadata.learnerId,
courseId: metadata.courseId,
attemptNumber: metadata.attemptNumber
});
return record?.stateData || null;
},

clearState: async (metadata) => {
await database.deleteSequencingState({
learnerId: metadata.learnerId,
courseId: metadata.courseId,
attemptNumber: metadata.attemptNumber
});
return true;
}
},

autoSaveOn: 'commit', // Save on every commit
compress: true, // Compress state data
maxStateSize: 50000 // Max 50KB
}
});

Handling Activity Transitions

// Listen for sequencing events
api.on('SequenceNext', () => {
// Library handles this internally if sequencing is configured
// The onActivityDelivery callback will be invoked
});

api.on('SequenceChoice', (eventName, element, targetId) => {
// Content requested specific activity
console.log(`Choice navigation to: ${targetId}`);
});

api.on('SequenceExit', () => {
// Exit current activity
});

api.on('SequenceExitAll', () => {
// Exit entire course
showCourseExitScreen();
});

// When transitioning between activities:
async function onActivityDelivery(activity) {
// 1. Get previous state for this activity
const previousState = await getScoAttemptState(
registration.currentAttemptId,
activity.id
);

// 2. Get global objectives
const globalObjectives = await prepareObjectivesForLaunch(
activity.id,
registration.currentAttemptId,
registration.globalObjectiveIds
);

// 3. Reset API for new activity
api.reset({
// Preserve sequencing settings
sequencing: api.settings.sequencing
});

// 4. Load new activity data
api.loadFromJSON({
cmi: {
learner_id: learner.id,
learner_name: learner.displayName,
entry: previousState?.suspendData ? "resume" : "ab-initio",
// ... other CMI data from previousState
objectives: globalObjectives
}
});

// 5. Launch content
contentFrame.src = activity.launchUrl;
}

Commit Endpoint Specification

Request Format

URL: Configurable via lmsCommitUrl setting

Method: POST

Content-Type: Depends on commitRequestDataType setting (default: application/json)

Body Format: Depends on dataCommitFormat setting:

JSON Format (default, dataCommitFormat: "json")

{
"successStatus": "passed",
"completionStatus": "completed",
"totalTimeSeconds": 3600,
"score": {
"raw": 85,
"min": 0,
"max": 100,
"scaled": 0.85
},
"runtimeData": {
"cmi": {
"completion_status": "completed",
"success_status": "passed",
"location": "page_15",
"suspend_data": "base64encodeddata...",
"score": {
"scaled": "0.85",
"raw": "85",
"min": "0",
"max": "100"
},
"objectives": {
"0": {
"id": "obj-1",
"success_status": "passed",
"completion_status": "completed",
"score": { "scaled": "0.9" }
}
},
"interactions": {
"0": {
"id": "q1",
"type": "choice",
"learner_response": "a",
"result": "correct"
}
}
},
"adl": {
"nav": {
"request": "continue"
}
}
},
"courseId": "course-123",
"scoId": "sco-456",
"learnerId": "learner-789",
"learnerName": "John Smith"
}

Flattened Format (dataCommitFormat: "flattened")

{
"cmi.completion_status": "completed",
"cmi.success_status": "passed",
"cmi.score.scaled": "0.85",
"cmi.score.raw": "85",
"cmi.objectives.0.id": "obj-1",
"cmi.objectives.0.success_status": "passed"
}

Params Format (dataCommitFormat: "params")

cmi.completion_status=completed&cmi.success_status=passed&cmi.score.scaled=0.85

Response Format

Success Response:

{
"result": "true"
}

Error Response:

{
"result": "false",
"errorCode": 391,
"errorMessage": "Database write failed"
}

Termination Commits

On Terminate() / LMSFinish(), the library uses navigator.sendBeacon() for reliability. This is a fire-and-forget request that doesn't wait for response.

Important: Ensure your endpoint can handle beacon requests (no response expected, small payload).

Custom Response Handling

const api = new Scorm2004API({
lmsCommitUrl: "/api/scorm/commit",

// For synchronous XMLHttpRequest (default)
xhrResponseHandler: (xhr) => {
const response = JSON.parse(xhr.responseText);
return {
result: response.success ? "true" : "false",
errorCode: response.error?.code || 0
};
},

// For fetch API (if useAsynchronousCommits: true)
responseHandler: async (response) => {
const data = await response.json();
return {
result: data.success ? "true" : "false",
errorCode: data.error?.code || 0
};
}
});

Entry Mode Decision Logic

The entry field tells the SCO whether this is a fresh start or resume.

Decision Matrix

ConditionEntry Value
First launch ever, no previous data"ab-initio"
Previous exit was "suspend""resume"
Previous exit was "suspend" with suspend_data"resume"
Previous exit was "" (normal)"ab-initio" or ""
Previous exit was "logout""ab-initio" or ""
Previous exit was "time-out" (SCORM 2004)LMS decision
Learner explicitly starting new attempt"ab-initio"
Lesson mode is "review""" (empty)

Implementation

function determineEntry(previousState, lessonMode) {
// Review mode doesn't use entry
if (lessonMode === "review") {
return "";
}

// No previous state = new attempt
if (!previousState) {
return "ab-initio";
}

// Check exit mode from previous session
const exitMode = previousState.exitMode;

// SCORM 1.2
if (exitMode === "suspend" || previousState.suspendData) {
return "resume";
}

// SCORM 2004 adds more exit types
if (exitMode === "suspend") {
return "resume";
}

// Normal completion or logout = new attempt
return "ab-initio";
}

Error Handling

API Error Codes

The library returns specific error codes. Your endpoint should handle:

CodeMeaningLMS Action
0No errorSuccess
101General exceptionLog and retry
301Not initializedLog error
391General commit failureRetry with backoff
405Write-only elementN/A (API prevents)

Commit Failure Handling

// The library handles commit failures internally
// Your endpoint should return meaningful errors

async function commitHandler(req, res) {
try {
await saveData(req.body);
res.json({ result: "true" });
} catch (error) {
console.error('Commit failed:', error);

// Return structured error
res.status(200).json({ // Note: Still 200 status
result: "false",
errorCode: 391,
errorMessage: error.message
});
}
}

Network Failure Handling

For termination commits using sendBeacon:

  • No response is received
  • Implement server-side retry logic
  • Use idempotency keys to prevent duplicates
const api = new Scorm2004API({
lmsCommitUrl: "/api/scorm/commit",

// Transform request to include idempotency key
requestHandler: (commitObject) => {
return {
...commitObject,
idempotencyKey: generateUUID(),
timestamp: Date.now()
};
}
});

Session Management

Multiple Concurrent Sessions

If a learner might have multiple browser tabs/windows:

  1. Use session identifiers
const sessionId = generateUUID();
api.loadFromJSON({
cmi: {
// ... other data
}
});

// Include session ID in commits via requestHandler
  1. Detect stale sessions
// On commit, check if this session is still active
async function commitHandler(data) {
const activeSession = await getActiveSession(data.learnerId, data.courseId);

if (activeSession && activeSession.id !== data.sessionId) {
// Another session is active
return {
result: "false",
errorCode: 391,
errorMessage: "Session expired"
};
}

// Process commit...
}

Browser Close Handling

The library uses sendBeacon for termination to survive browser close, but:

  1. Implement auto-commit
const api = new Scorm2004API({
autocommit: true,
autocommitSeconds: 30 // Commit every 30 seconds
});
  1. Handle incomplete sessions
// Scheduled job to clean up abandoned sessions
async function cleanupAbandonedSessions() {
const staleThreshold = Date.now() - (30 * 60 * 1000); // 30 minutes

const staleSessions = await database.findSessions({
lastCommitAt: { $lt: new Date(staleThreshold) },
status: 'active'
});

for (const session of staleSessions) {
await database.updateSession(session.id, {
status: 'abandoned',
exitMode: 'abnormal'
});
}
}

Integration Checklists

SCORM 1.2 Single-SCO Checklist

Package Import:

  • Parse <organization> for structure
  • Extract <item> metadata (title, identifier, identifierref)
  • Parse <adlcp:masteryscore> from resources
  • Parse <adlcp:maxtimeallowed>
  • Parse <adlcp:timelimitaction>
  • Parse <adlcp:datafromlms>
  • Resolve launch URL from resource href

Storage Setup:

  • Create Course record
  • Create CourseItem records
  • Learner registration mechanism
  • ScoAttemptState storage

Runtime - Launch:

  • Create/configure Scorm12API instance
  • Set lmsCommitUrl
  • Load learner info (student_id, student_name)
  • Load previous state if resuming
  • Load manifest data (mastery_score, launch_data, etc.)
  • Determine entry mode
  • Determine lesson_mode and credit
  • Attach API to window.API
  • Launch SCO in iframe

Runtime - Commit:

  • Endpoint receives POST requests
  • Parse CMI data from request body
  • Extract lesson_status, location, suspend_data
  • Extract score data
  • Parse time values
  • Save to ScoAttemptState
  • Return success/failure response

Runtime - Termination:

  • Handle sendBeacon requests
  • Update final state
  • Calculate total time
  • Update registration status

SCORM 1.2 Multi-SCO Checklist

All of the above, plus:

Package Import:

  • Parse all <item> elements (ordered)
  • Handle nested items if present
  • Parse <prerequisites> elements
  • Store sequence/order information

Storage:

  • Track state per SCO
  • Store current SCO pointer
  • Course-level rollup fields

Runtime:

  • Enable globalStudentPreferences: true
  • Implement SCO navigation (next/previous)
  • Implement prerequisite checking
  • Call api.reset() between SCOs
  • Calculate course rollup on commit

SCORM 2004 Single-SCO Checklist

Package Import:

  • Parse SCORM 2004 namespace elements
  • Extract <adlcp:completionThreshold>
  • Extract <imsss:minNormalizedMeasure> (scaled passing score)
  • Parse time limits (ISO 8601 format)

Storage:

  • Support larger suspend_data (64KB)
  • Store scaled scores (-1 to 1)
  • Store progress_measure
  • Separate completion_status and success_status
  • Store objectives with full data model
  • Store interactions with full data model

Runtime:

  • Create Scorm2004API instance
  • Attach to window.API_1484_11
  • Load all SCORM 2004 CMI fields
  • Handle auto-completion based on threshold
  • Handle auto-pass/fail based on scaled_passing_score

SCORM 2004 Sequenced Checklist

All of the above, plus:

Package Import:

  • Parse <imsss:sequencing> elements
  • Parse <imsss:controlMode> attributes
  • Parse <imsss:sequencingRules> (pre/post/exit)
  • Parse <imsss:rollupRules>
  • Parse <imsss:objectives> with <imsss:mapInfo>
  • Identify global objectives
  • Parse <imsss:deliveryControls>
  • Build complete activity tree

Storage:

  • Store sequencing configuration
  • Store global objectives separately
  • Store per-activity state
  • Store sequencing runtime state

Runtime Configuration:

  • Configure sequencing.activityTree
  • Configure sequencing.eventListeners
  • Set scoItemIds or scoItemIdValidator
  • Set globalObjectiveIds
  • Configure sequencingStatePersistence if needed

Runtime Handling:

  • Handle onActivityDelivery callback
  • Handle onNavigationValidityUpdate for UI
  • Merge global objectives on SCO launch
  • Extract global objectives on commit
  • Reset API between activity transitions
  • Preserve sequencing state across sessions

API Settings Reference

This section documents all configuration settings available when creating API instances.

Core Settings

SettingTypeDefaultDescription
lmsCommitUrlstring | falsefalseURL for commit endpoint. Set to false to disable HTTP commits.
autocommitbooleanfalseEnable periodic auto-commit to LMS.
autocommitSecondsnumber60Interval in seconds between auto-commits.
dataCommitFormatstring"json"Format for commit data: "json", "flattened", or "params".
commitRequestDataTypestring"application/json"Content-Type header for commit requests.
sendFullCommitbooleantrueSend full CMI data on every commit vs. only changed values.

HTTP Settings

SettingTypeDefaultDescription
useAsynchronousCommitsbooleanfalseUse async fetch instead of sync XMLHttpRequest. Note: Async mode returns optimistic success; actual result comes via events.
throttleCommitsbooleanfalseThrottle rapid successive commits (only with async mode).
xhrHeadersobject{}Custom headers to include in commit requests.
xhrWithCredentialsbooleanfalseInclude credentials (cookies) in cross-origin requests.
fetchModestring"cors"Fetch mode for async requests: "cors", "no-cors", "same-origin", "navigate".
useBeaconInsteadOfFetchstring"on-terminate"When to use sendBeacon: "always", "on-terminate", "never".
httpServiceIHttpService | nullnullCustom HTTP service implementation.

Response Handlers

SettingTypeDefaultDescription
responseHandlerfunctionBuilt-inCustom handler for fetch responses: (response: Response) => Promise<ResultObject>
xhrResponseHandlerfunctionBuilt-inCustom handler for XHR responses: (xhr: XMLHttpRequest) => ResultObject
requestHandlerfunctionIdentityTransform commit object before sending: (commitObject) => transformedObject

Logging

SettingTypeDefaultDescription
logLevelLogLevel4 (ERROR)Logging verbosity: 1/"DEBUG", 2/"INFO", 3/"WARN", 4/"ERROR", 5/"NONE"
onLogMessagefunctionundefinedCustom log handler: (level: LogLevel, message: string) => void

SCORM 1.2 Specific Settings

SettingTypeDefaultDescription
mastery_overridebooleantrueWhen mastery_score is set, auto-set lesson_status to "passed"/"failed" based on score.
autoCompleteLessonStatusbooleantrueAuto-set lesson_status to "completed" on LMSFinish if no status was set by content.
globalStudentPreferencesbooleanfalseShare student_preference values across SCOs within a session (for multi-SCO).

SCORM 2004 Specific Settings

SettingTypeDefaultDescription
score_overrides_statusbooleanfalseAllow score to override success_status even when status is already set.
completion_status_on_failedstringundefinedSet completion_status to this value ("completed" or "incomplete") when success_status is "failed".
autoProgressbooleanfalseAuto-progress through activities on completion (requires sequencing).

Time Settings

SettingTypeDefaultDescription
selfReportSessionTimebooleanfalseAllow content to set session_time directly instead of library calculating it.
alwaysSendTotalTimebooleanfalseAlways include total_time in commits, even if unchanged.

Error Handling

SettingTypeDefaultDescription
strict_errorsbooleantrueStrictly validate all data model operations per SCORM spec.

Commit Metadata Settings

SettingTypeDefaultDescription
renderCommonCommitFieldsbooleantrueInclude successStatus, completionStatus, totalTimeSeconds, score at top level of commit.
autoPopulateCommitMetadatabooleanfalseInclude courseId, scoId, learnerId, learnerName in commit object.
courseIdstringundefinedCourse identifier for commit metadata.
scoIdstringundefinedSCO identifier for commit metadata.
SettingTypeDefaultDescription
scoItemIdsstring[]undefinedValid SCO item IDs for navigation validation.
scoItemIdValidatorfunction | falseundefinedCustom validator: (scoItemId: string) => boolean. Set to false to disable.
globalObjectiveIdsstring[]undefinedIDs of objectives shared across SCOs.

Sequencing Settings (SCORM 2004)

SettingTypeDefaultDescription
sequencingSequencingSettingsundefinedFull sequencing configuration object. See Sequencing Configuration.
sequencingStatePersistenceobjectundefinedCallbacks for persisting/loading sequencing state. See Sequencing State Persistence.

Offline Support Settings

SettingTypeDefaultDescription
enableOfflineSupportbooleanfalseEnable offline data storage and sync.
syncOnInitializebooleantrueAttempt to sync offline data on Initialize.
syncOnTerminatebooleantrueAttempt to sync offline data on Terminate.
maxSyncAttemptsnumber3Maximum retry attempts for sync operations.

Sequencing Events Reference (SCORM 2004)

The sequencing engine emits events for LMS coordination. Configure listeners via sequencing.eventListeners.

EventCallback SignatureDescription
onNavigationRequest(request: string, target?: string) => voidNavigation request received from content (continue, previous, choice, etc.)
onNavigationRequestProcessing(data: {request, targetActivityId}) => voidNavigation request is being processed
onNavigationValidityUpdate(data: {currentActivity, validRequests}) => voidValid navigation options changed (for UI updates)

Activity Lifecycle Events

EventCallback SignatureDescription
onSequencingStart(activity: Activity) => voidSequencing session started
onSequencingEnd() => voidSequencing session ended
onActivityDelivery(activity: Activity) => voidCritical: Activity should be delivered to learner
onActivityUnload(activity: Activity) => voidActivity is being unloaded
onActivitySuspended(data: {activity}) => voidActivity was suspended
onSuspendedActivityCleanup(data: {activity}) => voidSuspended activity state was cleaned up

Rollup Events

EventCallback SignatureDescription
onRollupComplete(activity: Activity) => voidRollup calculation completed
onAutoCompletion(data: {activity, completionStatus}) => voidActivity auto-completed based on children
onAutoSatisfaction(data: {activity, satisfiedStatus}) => voidActivity auto-satisfied based on measure

Termination Events

EventCallback SignatureDescription
onTerminationRequestProcessing(data: {request, hasSequencingRequest, currentActivity}) => voidTermination request being processed
onSequencingSessionEnd(data: {reason, exception?, navigationRequest?}) => voidSequencing session ended with details
onPostConditionEvaluated(data: {activity, result, iteration}) => voidPost-condition rule evaluated
onPostConditionExitParent(data: {activity}) => voidPost-condition triggered exit to parent
onPostConditionExitAll(data: {activity}) => voidPost-condition triggered exit all
onMultiLevelExitAction(data: {activity}) => voidMulti-level exit action triggered

Delivery Events

EventCallback SignatureDescription
onDeliveryRequestProcessing(data: {request, target}) => voidDelivery request being processed

Global Objective Events

EventCallback SignatureDescription
onGlobalObjectiveMapInitialized(data: {count}) => voidGlobal objective map initialized
onGlobalObjectiveMapError(data: {error}) => voidError initializing global objectives
onGlobalObjectiveUpdated(data: {objectiveId, field, value}) => voidGlobal objective value updated
onGlobalObjectiveUpdateError(data: {objectiveId, error}) => voidError updating global objective

Limit Condition Events

EventCallback SignatureDescription
onLimitConditionCheck(data: {activity, limitType, exceeded}) => voidLimit condition checked

Error and Debug Events

EventCallback SignatureDescription
onSequencingError(error: string, context?: string) => voidSequencing error occurred
onSuspendError(data: {activity, error}) => voidError during suspend operation
onStateInconsistency(data: {activity, issue}) => voidState inconsistency detected
onSequencingDebug(data: {message, context?}) => voidDebug information (when logLevel is debug)

Event Configuration Example

const api = new Scorm2004API({
sequencing: {
activityTree: activityTree,
eventListeners: {
// Critical - handle activity delivery
onActivityDelivery: (activity) => {
console.log(`Launching: ${activity.id}`);
launchContent(activity);
},

// Update navigation UI
onNavigationValidityUpdate: (data) => {
setNavEnabled('continue', data.validRequests.includes('continue'));
setNavEnabled('previous', data.validRequests.includes('previous'));
},

// Track completion
onRollupComplete: (activity) => {
if (activity.id === 'root') {
console.log('Course rollup complete');
updateCourseStatus(activity);
}
},

// Handle session end
onSequencingSessionEnd: (data) => {
console.log(`Session ended: ${data.reason}`);
if (data.reason === 'exitAll') {
showCourseCompletionScreen();
}
},

// Debug during development
onSequencingDebug: (data) => {
console.debug('[Sequencing]', data.message, data.context);
}
}
}
});

Sequencing Configuration Reference

Activity Tree Structure

{
id: "root", // Activity identifier
title: "Course Title", // Display title
isVisible: true, // Visible in navigation
isActive: false, // Currently active
isSuspended: false, // Currently suspended
attemptLimit: null, // Max attempts (null = unlimited)
children: [ // Child activities
{
id: "module-1",
title: "Module 1",
children: [...],
sequencingRules: {...},
sequencingControls: {...},
rollupRules: {...},
objectives: [...]
}
],
sequencingControls: {...},
sequencingRules: {...},
rollupRules: {...}
}

Sequencing Controls

{
enabled: true, // Enable sequencing for this activity
choice: true, // Allow direct choice navigation
choiceExit: true, // Allow exit via choice
flow: true, // Enable continue/previous
forwardOnly: false, // Restrict to forward navigation only
preventActivation: false, // Prevent activity activation
constrainChoice: false, // Constrain choice to available activities
stopForwardTraversal: false, // Stop forward traversal at this activity

// Rollup controls
rollupObjectiveSatisfied: true, // Include in objective satisfaction rollup
rollupProgressCompletion: true, // Include in progress completion rollup
objectiveMeasureWeight: 1.0, // Weight for measure rollup

// Selection and randomization
selectionTiming: "once", // When to select: "once", "onEachNewAttempt"
selectCount: null, // Number of children to select (null = all)
randomizeChildren: false, // Randomize child order
randomizationTiming: "once", // When to randomize

// Attempt info
useCurrentAttemptObjectiveInfo: true, // Use current attempt for objective info
useCurrentAttemptProgressInfo: true, // Use current attempt for progress info

// Delivery controls (for leaf activities)
completionSetByContent: true, // Content sets completion (vs. LMS auto-set)
objectiveSetByContent: true // Content sets objective (vs. LMS auto-set)
}

Sequencing Rules

{
preConditionRules: [ // Evaluated before delivery
{
action: "skip", // "skip", "disabled", "hiddenFromChoice", "stopForwardTraversal"
conditionCombination: "all", // "all" (AND), "any" (OR)
conditions: [
{
condition: "satisfied", // See condition types below
operator: "not", // "not" or empty for positive
referencedObjective: "obj-1" // Objective to check (if applicable)
}
]
}
],
postConditionRules: [ // Evaluated after completion
{
action: "exitParent", // "exitParent", "exitAll", "retry", "retryAll", "continue", "previous"
conditions: [...]
}
],
exitConditionRules: [ // Evaluated on exit
{
action: "exit",
conditions: [...]
}
]
}

Rule Condition Types:

  • satisfied, objectiveStatusKnown, objectiveMeasureKnown, objectiveMeasureGreaterThan, objectiveMeasureLessThan
  • completed, activityProgressKnown, attempted, attemptLimitExceeded
  • timeLimitExceeded, outsideAvailableTimeRange
  • always

Rollup Rules

{
rules: [
{
action: "satisfied", // "satisfied", "notSatisfied", "completed", "incomplete"
consideration: "all", // "all", "any", "none", "atLeastCount", "atLeastPercent"
minimumCount: 0, // For "atLeastCount"
minimumPercent: 0, // For "atLeastPercent"
conditions: [
{
condition: "completed", // "satisfied", "objectiveStatusKnown", "objectiveMeasureKnown",
// "completed", "activityProgressKnown", "attempted"
parameters: {} // Optional parameters
}
]
}
]
}

Objective Map Info (Global Objectives)

{
objectiveID: "local-objective",
satisfiedByMeasure: true,
minNormalizedMeasure: 0.8,
mapInfo: [
{
targetObjectiveID: "global-objective-1", // Global objective to map to
readSatisfiedStatus: true, // Read from global
writeSatisfiedStatus: true, // Write to global
readNormalizedMeasure: true,
writeNormalizedMeasure: true,
readCompletionStatus: false,
writeCompletionStatus: false,
readProgressMeasure: false,
writeProgressMeasure: false,
readRawScore: false,
writeRawScore: false,
readMinScore: false,
writeMinScore: false,
readMaxScore: false,
writeMaxScore: false
}
]
}

Commit Object Reference

The commit object sent to your endpoint contains these fields:

Top-Level Fields (when renderCommonCommitFields: true)

FieldTypeDescription
successStatusstring"passed", "failed", or "unknown"
completionStatusstring"completed", "incomplete", "not attempted", or "unknown"
totalTimeSecondsnumberAccumulated time across all sessions
scoreobjectOptional score object with raw, min, max, scaled

Metadata Fields (when autoPopulateCommitMetadata: true)

FieldTypeDescription
courseIdstringCourse identifier
scoIdstringSCO identifier
learnerIdstringLearner identifier
learnerNamestringLearner display name
sessionIdstringSession identifier
activityIdstringActivity identifier (sequenced courses)
attemptnumberAttempt number
commitIdstringUnique commit identifier

Runtime Data

The runtimeData field contains the full CMI tree:

{
runtimeData: {
cmi: {
// SCORM 2004 fields
completion_status: "completed",
success_status: "passed",
location: "page_15",
suspend_data: "...",
progress_measure: "0.85",
score: {
scaled: "0.85",
raw: "85",
min: "0",
max: "100"
},
total_time: "PT1H30M",
session_time: "PT0H15M",
exit: "suspend",

objectives: {
0: {
id: "obj-1",
success_status: "passed",
completion_status: "completed",
progress_measure: "1.0",
score: { scaled: "0.9" },
description: "Complete module 1"
}
},

interactions: {
0: {
id: "q1",
type: "choice",
timestamp: "2024-01-15T10:30:00Z",
weighting: "1",
learner_response: "a",
result: "correct",
latency: "PT0H0M5S",
description: "Question about topic X",
objectives: { 0: { id: "obj-1" } },
correct_responses: { 0: { pattern: "a" } }
}
},

comments_from_learner: {
0: {
comment: "Great course!",
location: "module-3",
timestamp: "2024-01-15T10:35:00Z"
}
},

learner_preference: {
audio_level: "1",
audio_captioning: "0",
delivery_speed: "1",
language: "en"
}
},

adl: {
nav: {
request: "continue", // Navigation request from content
request_valid: {
continue: "true",
previous: "true",
choice: { "sco-2": "true" }
}
},
data: { // Shared data across SCOs
0: {
id: "shared-data-1",
store: "stored value"
}
}
}
}
}

Commit Data Formats

JSON Format (dataCommitFormat: "json"): Standard nested JSON object as shown above.

Flattened Format (dataCommitFormat: "flattened"):

{
"cmi.completion_status": "completed",
"cmi.success_status": "passed",
"cmi.score.scaled": "0.85",
"cmi.objectives.0.id": "obj-1",
"cmi.objectives.0.success_status": "passed"
}

Params Format (dataCommitFormat: "params"):

cmi.completion_status=completed&cmi.success_status=passed&cmi.score.scaled=0.85

Beacon Behavior

On Terminate() / LMSFinish(), the library uses navigator.sendBeacon() by default (configurable via useBeaconInsteadOfFetch):

  • Fire-and-forget: No response is received or expected
  • Survives page unload: Ensures data is sent even if user closes browser
  • Small payload limit: ~64KB maximum (varies by browser)
  • Server considerations: Your endpoint should handle these requests idempotently
// To always use fetch (and block on Terminate):
new Scorm2004API({
useBeaconInsteadOfFetch: "never"
});

// To always use beacon (fire-and-forget for all commits):
new Scorm2004API({
useBeaconInsteadOfFetch: "always"
});

ADL Data Model Elements (SCORM 2004)

adl.nav.request

Used by content to request navigation:

ValueDescription
"continue"Move to next activity
"previous"Move to previous activity
"{target=<id>}choice"Navigate to specific activity by ID
"exit"Exit current activity
"exitAll"Exit all activities and end session
"abandon"Abandon current activity
"abandonAll"Abandon all activities
"suspendAll"Suspend all activities
"_none_"No navigation request
"jump"Jump navigation (SCORM 2004 4th edition)

adl.nav.request_valid

Read-only elements indicating navigation validity:

ElementTypeDescription
adl.nav.request_valid.continuestring"true" or "false"
adl.nav.request_valid.previousstring"true" or "false"
adl.nav.request_valid.choice.{target}string"true" or "false" for each target
adl.nav.request_valid.jump.{target}string"true" or "false" (4th edition)

adl.data

Shared data bucket for cross-SCO communication:

ElementAccessDescription
adl.data._countRONumber of data stores
adl.data.n.idROData store identifier
adl.data.n.storeRWData store content (max 64000 chars)

Error Codes Reference

When API calls fail, GetLastError() returns an error code. Use these tables to understand and handle errors.

SCORM 1.2 Error Codes

CodeNameMeaningLMS Action
0No ErrorOperation succeededNone required
101General ExceptionUnspecified errorLog and investigate
201Invalid Argument ErrorBad parameter to API callCheck API call syntax
202Element Cannot Have ChildrenRequested .children on leaf elementFix CMI path
203Element Not An ArrayRequested ._count on non-arrayFix CMI path
301Not InitializedAPI call before LMSInitialize()Initialize first
401Not ImplementedElement not supportedCheck element name
402Invalid Set ValueValue doesn't meet requirementsValidate value format
403Element Is Read OnlyTried to write read-only elementDon't write to this element
404Element Is Write OnlyTried to read write-only elementDon't read this element
405Incorrect Data TypeWrong value type for elementCheck value format

SCORM 2004 Error Codes

CodeNameMeaningLMS Action
0No ErrorOperation succeededNone required
101General ExceptionUnspecified errorLog and investigate
102General Initialization FailureInitialize() failedCheck server connectivity
103Already InitializedInitialize() called twiceIgnore or fix content
104Content Instance TerminatedAPI used after Terminate()Session ended, cannot continue
111General Termination FailureTerminate() failedLog error, data may be lost
112Termination Before InitializationTerminate() before Initialize()Fix content
113Termination After TerminationTerminate() called twiceIgnore
122Retrieve Before InitializationGetValue() before Initialize()Initialize first
123Retrieve After TerminationGetValue() after Terminate()Session ended
132Store Before InitializationSetValue() before Initialize()Initialize first
133Store After TerminationSetValue() after Terminate()Session ended
142Commit Before InitializationCommit() before Initialize()Initialize first
143Commit After TerminationCommit() after Terminate()Session ended
201General Argument ErrorInvalid API argumentCheck call syntax
301General Get FailureGetValue() failedCheck element path
351General Set FailureSetValue() failedCheck element/value
391General Commit FailureCommit() failedCheck server, retry
401Undefined Data Model ElementUnknown CMI elementCheck element spelling
402Unimplemented Data Model ElementElement not supportedUse different element
403Data Model Element Value Not InitializedReading unset elementSet value first or handle empty
404Data Model Element Is Read OnlyWriting read-only elementDon't write to this element
405Data Model Element Is Write OnlyReading write-only elementDon't read this element
406Data Model Element Type MismatchWrong value typeCheck expected format
407Data Model Element Value Out Of RangeValue outside valid rangeCheck min/max
408Data Model Dependency Not EstablishedMissing prerequisiteSet required fields first (e.g., ID before score)

Error Recovery Strategies

Transient Errors (391 - Commit Failure)

api.on("CommitError", function() {
const errorCode = api.GetLastError();
if (errorCode === "391") {
// Retry with exponential backoff
retryCommit(attempts + 1);
}
});

Session Errors (104, 123, 133, 143)

These indicate the session has ended. Do not retry - inform user or redirect:

if (["104", "123", "133", "143"].includes(errorCode)) {
showMessage("Your session has ended. Progress has been saved.");
redirectToCourseListing();
}

Content Errors (408 - Dependency)

Common with objectives - ensure ID is set first:

// Error 408 often means: set ID before other properties
api.SetValue("cmi.objectives.0.id", "obj_1"); // Set ID first!
api.SetValue("cmi.objectives.0.score.raw", "85");

Offline Support Overview

Enable offline support for mobile or unreliable connectivity scenarios.

Configuration

const api = new Scorm2004API({
enableOfflineSupport: true,
courseId: "course_123", // Required for offline storage key
syncOnInitialize: true, // Sync when SCO starts
syncOnTerminate: true, // Sync when SCO ends
maxSyncAttempts: 5 // Retries before giving up
});

Events

Listen for sync status:

api.on("OfflineDataSynced", function() {
hideOfflineIndicator();
});

api.on("OfflineDataSyncFailed", function() {
showOfflineWarning("Your progress is saved locally and will sync when online.");
});

How It Works

  1. When offline, commits are stored in IndexedDB
  2. On next Initialize (or when online), queued commits are sent
  3. Failed syncs retry up to maxSyncAttempts times
  4. Data persists locally until successfully synced

Data Model Practical Notes

Session Time vs Total Time

FieldPersistenceCalculation
session_timeDon't persistCalculated by API during session
total_timePersistAccumulated across all sessions

The API automatically calculates session_time from Initialize to Terminate. You persist total_time and provide it at launch; the API adds the current session time on commit.

Credit vs No-Credit Mode

Modecmi.creditEffect
Credit"credit"Score and status count toward completion
No-Credit"no-credit"Tracking only, doesn't affect completion

Set cmi.credit at launch based on whether this attempt should count.

Entry Mode Logic

if (hasSuspendData AND exitWas("suspend")) → entry = "resume"
else if (hasAttemptRecord) → entry = "" (re-entry)
else → entry = "ab-initio" (first time)

See Entry Mode Decision Logic for full logic.


Additional Resources


Version History

  • v1.1 - Added comprehensive Settings Reference, Sequencing Events Reference, Sequencing Configuration Reference, Commit Object Reference, and ADL Data Model Elements
  • v1.0 - Initial comprehensive guide