CMI Data Understanding Guide
CMI Data Understanding Guide
Overview
CMI (Computer Managed Instruction) data is the standardized data model used by SCORM to track learner progress, scores, and interactions. This guide explains how CMI data is structured, stored, and managed in the SCORM API.
What is CMI Data?
CMI data represents the state of a learner's interaction with SCORM content. It includes:
- Progress Information: Completion status, time spent, location
- Assessment Data: Scores, pass/fail status, attempts
- Interaction Data: Learner responses, interactions, objectives
- Navigation Data: Current location, suspend data, entry status
CMI Data Structure
SCORM 1.2 CMI Data Model
SCORM 1.2 uses dot-notation for CMI elements:
interface SCORM12CMIData {
// Core elements
'cmi.core.student_id': string;
'cmi.core.student_name': string;
'cmi.core.lesson_status': 'not attempted' | 'incomplete' | 'completed' | 'passed' | 'failed' | 'browsed';
'cmi.core.score.raw': string;
'cmi.core.score.max': string;
'cmi.core.score.min': string;
'cmi.core.total_time': string; // HHMMSS.SS format
'cmi.core.session_time': string; // HHMMSS.SS format
'cmi.core.entry': 'ab-initio' | 'resume' | '';
'cmi.core.exit': '' | 'time-out' | 'suspend' | 'logout' | 'normal';
'cmi.core.lesson_location': string;
'cmi.suspend_data': string;
'cmi.launch_data': string;
'cmi.comments': string;
'cmi.comments_from_lms': string;
// Objectives (cmi.objectives.n)
'cmi.objectives._count': string;
'cmi.objectives.n.id': string;
'cmi.objectives.n.score.raw': string;
'cmi.objectives.n.score.max': string;
'cmi.objectives.n.score.min': string;
'cmi.objectives.n.status': string;
// Interactions (cmi.interactions.n)
'cmi.interactions._count': string;
'cmi.interactions.n.id': string;
'cmi.interactions.n.type': string;
'cmi.interactions.n.timestamp': string;
'cmi.interactions.n.correct_responses.n.pattern': string;
'cmi.interactions.n.learner_response': string;
'cmi.interactions.n.result': string;
'cmi.interactions.n.latency': string;
}
SCORM 2004 CMI Data Model
SCORM 2004 uses a similar structure but with some differences:
interface SCORM2004CMIData {
// Core elements
'cmi.learner_id': string;
'cmi.learner_name': string;
'cmi.completion_status': 'unknown' | 'completed' | 'incomplete' | 'not attempted';
'cmi.success_status': 'unknown' | 'passed' | 'failed';
'cmi.score.scaled': string; // 0.0 to 1.0
'cmi.score.raw': string;
'cmi.score.min': string;
'cmi.score.max': string;
'cmi.total_time': string; // ISO 8601 duration (PT#H#M#S)
'cmi.session_time': string; // ISO 8601 duration
'cmi.entry': 'ab-initio' | 'resume' | '';
'cmi.exit': '' | 'time-out' | 'suspend' | 'logout' | 'normal';
'cmi.location': string;
'cmi.suspend_data': string;
'cmi.launch_data': string;
'cmi.comments': string;
'cmi.comments_from_lms': string;
// Objectives (cmi.objectives.n)
'cmi.objectives._count': string;
'cmi.objectives.n.id': string;
'cmi.objectives.n.score.scaled': string;
'cmi.objectives.n.score.raw': string;
'cmi.objectives.n.score.min': string;
'cmi.objectives.n.score.max': string;
'cmi.objectives.n.success_status': string;
'cmi.objectives.n.completion_status': string;
// Interactions (cmi.interactions.n)
'cmi.interactions._count': string;
'cmi.interactions.n.id': string;
'cmi.interactions.n.type': string;
'cmi.interactions.n.timestamp': string;
'cmi.interactions.n.correct_responses.n.pattern': string;
'cmi.interactions.n.learner_response': string;
'cmi.interactions.n.result': string;
'cmi.interactions.n.latency': string;
}
CMI Data Storage
Database Schema
CMI data is stored in the scorm_sessions table:
CREATE TABLE scorm_sessions (
id UUID PRIMARY KEY,
package_id UUID REFERENCES scorm_packages(id),
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
cmi_data JSONB DEFAULT '{}'::jsonb, -- Full CMI data model
completion_status VARCHAR(20), -- Normalized: 'not_attempted', 'incomplete', 'completed'
success_status VARCHAR(20), -- Normalized: 'unknown', 'passed', 'failed'
score JSONB, -- Normalized: { scaled, raw, min, max }
session_time VARCHAR(50), -- ISO 8601 duration
suspend_data TEXT, -- For resuming sessions
version INTEGER DEFAULT 1, -- Optimistic locking
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ
);
Normalized Fields
The API stores both raw CMI data and normalized fields for easier querying:
completion_status: Normalized fromcmi.core.lesson_status(1.2) orcmi.completion_status(2004)success_status: Normalized from lesson_status orcmi.success_statusscore: Normalized score object with scaled/raw/min/maxsession_time: ISO 8601 duration format
Reading CMI Data
Get Session Data
curl https://api.scorm.com/api/v1/sessions/{sessionId} \
-H "X-API-Key: your-key"
Response:
{
"id": "session-123",
"package_id": "package-456",
"user_id": "user-789",
"cmi_data": {
"cmi.core.lesson_status": "completed",
"cmi.core.score.raw": "85",
"cmi.core.score.max": "100",
"cmi.core.score.min": "0",
"cmi.core.total_time": "001530:45.00",
"cmi.core.session_time": "001530:45.00",
"cmi.core.lesson_location": "page_5",
"cmi.suspend_data": ""
},
"completion_status": "completed",
"success_status": "passed",
"score": {
"raw": 85,
"min": 0,
"max": 100,
"scaled": 0.85
},
"session_time": "PT1H30M45S",
"time_spent_seconds": 5445,
"attempts": 1,
"created_at": "2025-01-12T10:00:00Z",
"updated_at": "2025-01-12T11:30:45Z"
}
Programmatic Access
interface Session {
id: string;
package_id: string;
user_id: string;
cmi_data: Record<string, any>; // Full CMI data
completion_status: 'not_attempted' | 'incomplete' | 'completed';
success_status: 'unknown' | 'passed' | 'failed';
score: {
scaled?: number;
raw?: number;
min?: number;
max?: number;
};
session_time: string;
time_spent_seconds: number;
}
async function getSessionData(sessionId: string): Promise<Session> {
const response = await fetch(`https://api.scorm.com/api/v1/sessions/${sessionId}`, {
headers: { 'X-API-Key': apiKey }
});
return response.json();
}
// Access CMI data
const session = await getSessionData(sessionId);
const lessonStatus = session.cmi_data['cmi.core.lesson_status'];
const score = session.cmi_data['cmi.core.score.raw'];
Updating CMI Data
Update Session
curl -X PUT https://api.scorm.com/api/v1/sessions/{sessionId} \
-H "X-API-Key: your-key" \
-H "Content-Type: application/json" \
-d '{
"cmi_data": {
"cmi.core.lesson_status": "completed",
"cmi.core.score.raw": "90"
},
"version": 1
}'
CMI Data Merging
The API automatically merges CMI data updates:
// Current session data
const current = {
cmi_data: {
'cmi.core.lesson_status': 'incomplete',
'cmi.core.score.raw': '75',
'cmi.core.lesson_location': 'page_3'
}
};
// Update request
const update = {
cmi_data: {
'cmi.core.lesson_status': 'completed',
'cmi.core.score.raw': '90'
}
};
// Result: Merged data
const merged = {
'cmi.core.lesson_status': 'completed', // Updated
'cmi.core.score.raw': '90', // Updated
'cmi.core.lesson_location': 'page_3' // Preserved
};
Optimistic Locking
Updates must include the current version to prevent conflicts:
async function updateCMIData(sessionId: string, updates: any) {
// 1. Get current session
const session = await getSessionData(sessionId);
// 2. Merge CMI data
const mergedCmiData = {
...session.cmi_data,
...updates.cmi_data
};
// 3. Update with version
const response = await fetch(`/api/v1/sessions/${sessionId}`, {
method: 'PUT',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
...updates,
cmi_data: mergedCmiData,
version: session.version // Current version
})
});
if (response.status === 409) {
// Version conflict - retry with fresh data
return updateCMIData(sessionId, updates);
}
return response.json();
}
Key CMI Elements
Completion Status
SCORM 1.2:
cmi.core.lesson_status:'not attempted','incomplete','completed','passed','failed','browsed'
SCORM 2004:
cmi.completion_status:'unknown','completed','incomplete','not attempted'cmi.success_status:'unknown','passed','failed'
Normalized:
completion_status:'not_attempted','incomplete','completed'success_status:'unknown','passed','failed'
Scores
SCORM 1.2:
{
'cmi.core.score.raw': '85',
'cmi.core.score.max': '100',
'cmi.core.score.min': '0'
}
SCORM 2004:
{
'cmi.score.scaled': '0.85', // 0.0 to 1.0
'cmi.score.raw': '85',
'cmi.score.max': '100',
'cmi.score.min': '0'
}
Normalized:
{
scaled: 0.85,
raw: 85,
min: 0,
max: 100
}
Time Tracking
SCORM 1.2 Format: HHMMSS.SS
- Example:
"001530:45.00"= 1 hour, 30 minutes, 45 seconds
SCORM 2004 Format: ISO 8601 Duration PT#H#M#S
- Example:
"PT1H30M45S"= 1 hour, 30 minutes, 45 seconds
Normalized: ISO 8601 duration + time_spent_seconds (integer)
Suspend Data
Suspend data allows learners to resume sessions:
// Save suspend data
const suspendData = JSON.stringify({
currentPage: 5,
bookmarks: [1, 3, 5],
notes: 'Learner notes'
});
// Update session
await updateSession(sessionId, {
cmi_data: {
'cmi.suspend_data': suspendData
}
});
// Resume later
const session = await getSessionData(sessionId);
const savedState = JSON.parse(session.cmi_data['cmi.suspend_data']);
Common CMI Operations
Check Completion
function isCompleted(session: Session): boolean {
return session.completion_status === 'completed';
}
function hasPassed(session: Session): boolean {
return session.success_status === 'passed';
}
Calculate Score Percentage
function getScorePercentage(session: Session): number | null {
if (!session.score.raw || !session.score.max) {
return null;
}
return (session.score.raw / session.score.max) * 100;
}
Format Time Spent
function formatTimeSpent(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return `${hours}h ${minutes}m ${secs}s`;
}
Track Progress
function getProgress(session: Session): {
completed: boolean;
passed: boolean;
score: number | null;
timeSpent: string;
} {
return {
completed: session.completion_status === 'completed',
passed: session.success_status === 'passed',
score: session.score.scaled ? session.score.scaled * 100 : null,
timeSpent: formatTimeSpent(session.time_spent_seconds)
};
}
CMI Data Best Practices
1. Always Merge Updates
Never replace entire CMI data - always merge:
// ❌ Bad: Replaces all data
await updateSession(sessionId, {
cmi_data: { 'cmi.core.lesson_status': 'completed' }
});
// ✅ Good: Merges with existing data
const session = await getSessionData(sessionId);
await updateSession(sessionId, {
cmi_data: {
...session.cmi_data,
'cmi.core.lesson_status': 'completed'
},
version: session.version
});
2. Handle Version Conflicts
Always implement retry logic for version conflicts:
async function updateWithRetry(sessionId: string, updates: any, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const session = await getSessionData(sessionId);
const merged = { ...session.cmi_data, ...updates.cmi_data };
return await updateSession(sessionId, {
...updates,
cmi_data: merged,
version: session.version
});
} catch (error) {
if (error.code === 'VERSION_CONFLICT' && attempt < maxRetries - 1) {
continue; // Retry
}
throw error;
}
}
}
3. Validate CMI Values
Validate CMI data before updating:
function validateCMIValue(element: string, value: string): boolean {
// SCORM 1.2 lesson_status validation
if (element === 'cmi.core.lesson_status') {
const valid = ['not attempted', 'incomplete', 'completed', 'passed', 'failed', 'browsed'];
return valid.includes(value);
}
// Score validation
if (element.includes('score.raw')) {
const num = parseFloat(value);
return !isNaN(num) && num >= 0;
}
return true;
}
4. Use Normalized Fields for Queries
Use normalized fields (completion_status, success_status, score) for filtering and reporting:
// Query completed sessions
const completed = await querySessions({
completion_status: 'completed'
});
// Query passed sessions
const passed = await querySessions({
success_status: 'passed'
});
// Query high scores
const highScores = await querySessions({
'score.scaled': { $gte: 0.8 }
});
CMI Data Conversion
SCORM 1.2 to 2004 Mapping
function convert12To2004(cmi12: SCORM12CMIData): SCORM2004CMIData {
return {
'cmi.learner_id': cmi12['cmi.core.student_id'],
'cmi.learner_name': cmi12['cmi.core.student_name'],
'cmi.completion_status': mapLessonStatus(cmi12['cmi.core.lesson_status']),
'cmi.success_status': mapSuccessStatus(cmi12['cmi.core.lesson_status']),
'cmi.score.raw': cmi12['cmi.core.score.raw'],
'cmi.score.max': cmi12['cmi.core.score.max'],
'cmi.score.min': cmi12['cmi.core.score.min'],
'cmi.total_time': convertTimeFormat(cmi12['cmi.core.total_time']),
'cmi.session_time': convertTimeFormat(cmi12['cmi.core.session_time']),
'cmi.location': cmi12['cmi.core.lesson_location'],
'cmi.suspend_data': cmi12['cmi.suspend_data']
};
}
Troubleshooting
Issue: CMI Data Not Persisting
Causes:
- Version conflict not handled
- Missing
LMSCommit()call - Network errors during update
Solutions:
- Implement retry logic for version conflicts
- Ensure
LMSCommit()is called after updates - Add error handling and logging
Issue: Incorrect Score Calculation
Causes:
- Score values as strings instead of numbers
- Missing min/max values
- Incorrect scaled score calculation
Solutions:
- Parse score values as numbers
- Ensure min/max are set
- Calculate scaled:
(raw - min) / (max - min)
Issue: Time Tracking Inaccurate
Causes:
- Time format conversion errors
- Session time not updated
- Clock synchronization issues
Solutions:
- Use ISO 8601 duration format
- Update session time on each interaction
- Use server time for calculations
Last Updated: 2025-01-12
Version: 1.0
For session management, see Session Management Guide.