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 from cmi.core.lesson_status (1.2) or cmi.completion_status (2004)
  • success_status: Normalized from lesson_status or cmi.success_status
  • score: Normalized score object with scaled/raw/min/max
  • session_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.