Skip to main content

MSK Assessment Integration Guide

This guide covers integrating the GOFA MSK (Musculoskeletal) Assessment module within your Flutter app using the GOFA WebView SDK.

Overview

The MSK Assessment module provides comprehensive musculoskeletal health evaluations including:

  • Assessment Survey: Initial questionnaire about symptoms and history
  • Physical Assessment: Guided physical tests and measurements
  • Pain Review: Detailed pain location and intensity mapping
  • Report Generation: Comprehensive assessment results and recommendations

Event-Driven Integration

The MSK assessment communicates with your Flutter app through events posted via the WebView SDK's updateCallback function. These events allow you to:

  • Track assessment progress
  • Handle user interactions
  • Access generated reports
  • Respond to early exits or completion

Key changes (Aug 2025)

  • No state field is sent. Completion is inferred by the presence of specific data (e.g., when risk.* exists, the assessment is considered ended).
  • Every event includes BOTH of the following when available:
    • mskAssessResult: the original nested JSON shape as stored by the app (primary/main result).
    • mskAssessResultFlattened: a flattened snapshot of the assessment result (dot-notation keys, assessmentData. prefix removed, createdAt converted to createdAt._seconds/_nanoseconds when applicable).
  • The older, separate nested payloads (surveyResult / physicalResult / painReviewResult) are deprecated and are not sent.
note

Your Flutter app receives events via the WebView SDK as an object with { type, data }. The type matches the event names below and data contains the payload.

Event Flow

Implementation

Basic Event Handler

void _handleMSKEvents(String event, dynamic data) {
switch (event) {
case 'START_MSK_ASSESSMENT':
_onAssessmentStarted(data);
break;
case 'FINISH_ASSESSMENT_SURVEY':
_onSurveyCompleted(data);
break;
case 'FINISH_PHYSICAL_ASSESSMENT':
_onPhysicalCompleted(data);
break;
case 'FINISH_PAIN_REVIEW':
_onPainReviewCompleted(data);
break;
case 'GENERATED_REPORT':
// data.reportUrl is a base64 data URL (data:application/pdf;base64,...)
_onReportGenerated(data);
break;
case 'EARLY_QUIT_ASSESSMENT':
_onEarlyQuit(data);
break;
case 'QUIT_MSK_MODULE':
_onModuleQuit();
break;
}
}
tip

From each handler, you can read either data['mskAssessResult'] (nested/original) or data['mskAssessResultFlattened'] (flattened). Choose the one that best fits your storage/analytics needs.

Working with nested vs flattened results

  • mskAssessResult (nested/original - primary)
    • Matches the domain model stored by the app; includes assessmentData subtree and optional risk.
  • mskAssessResultFlattened (flattened)
    • Dot-notation keys for nested fields.
    • assessmentData. prefix removed (e.g., assessmentData.assessmentSurvey.age -> assessmentSurvey.age).
    • createdAt appears as createdAt._seconds and createdAt._nanoseconds when applicable.

Example keys (flattened):

{
"assessmentSurvey.age": 45,
"assessmentResults.rangeOfMotion": 85.5,
"painReview.abductionMaxPain.right": "severe",
"risk.SIS.riskLevel": "MODERATE"
}
Completion detection

When risk.* exists in either the nested or flattened payload, the assessment is considered ended.

Deprecated payloads

The older per-phase payloads surveyResult, physicalResult, and painReviewResult are no longer sent. Use mskAssessResult or mskAssessResultFlattened instead.

Assessment Data Reference

The MSK module supports three assessment codes, each with its own survey questions, physical tests, pain review, and risk calculations:

Assessment CodeDescriptionRisk Types
shoulderShoulder assessmentSIS (Shoulder Impingement Syndrome), FS (Frozen Shoulder)
low-backLow back assessmentLB (Low Back Muscle Strain), LDC (Lumbar Degenerative Compression), LDH (Lumbar Disc Herniation)
kneeKnee assessmentOA (Knee Osteoarthritis), PFPS (Patellofemoral Pain Syndrome)

Common Base Fields

All assessment results share these base fields:

KeyTypeDescription
idstringUnique assessment result ID
clientIdstringClient/tenant identifier
userIdstring (optional)User identifier
assessmentCode"shoulder" | "low-back" | "knee"Assessment type
createdAtISO string (nested) or ._seconds/._nanoseconds (flattened)Creation timestamp
assessmentDataobjectContains assessmentSurvey, assessmentResults, painReview
riskobjectRisk scores per condition (present when assessment is complete)

Shoulder Assessment (shoulder)

Nested (mskAssessResult) Example — Shoulder

{
"id": "523bOJ5ky183Qr5qZ61B",
"clientId": "bupa-uat",
"userId": "mzkQP97YwpdjpmWS7a9qZTB4w9t1",
"assessmentCode": "shoulder",
"createdAt": "2025-08-07T12:32:58.031Z",
"assessmentData": {
"assessmentSurvey": {
"version": "SHOULDER_ASSESSMENT_SURVEY_CONTENT_V1",
"shoulderPainRecurring": false,
"shoulderPainRelievedByRest": false,
"shoulderPainOverheadActivity": true,
"gender": "male",
"shoulderPainInModerateMotion": false,
"shoulderPainFixedPosition": false,
"shoulderPainWithHandBehindBack": false,
"shoulderPainRepetitiveMotion": false,
"shoulderPainWithOverheadActivity": true,
"shoulderPainAtLimit": true,
"shoulderPainNoInjury": true,
"diabetes": "none",
"age": 45,
"shoulderRangeOfMotionLimited": false
},
"assessmentResults": {
"version": "SHOULDER_ASSESSMENT_RESULTS_V1",
"rangeOfMotion": 85.5,
"strengthScore": 78.2,
"abduction": { "left": 180, "right": 180 },
"flexion": { "left": 160, "right": 160 },
"extension": { "left": 60, "right": 60 },
"externalRotation": { "left": 90, "right": 90 },
"internalRotation": { "left": 70, "right": 70 },
"compression": { "left": 45, "right": 45 }
},
"painReview": {
"version": "SHOULDER_PAIN_REVIEW_CONTENT_V1",
"flexionMidPain": { "left": "moderate", "right": "moderate" },
"flexionMaxPain": { "left": "moderate", "right": "moderate" },
"abductionMidPain": { "left": "moderate", "right": "moderate" },
"abductionMaxPain": { "left": "mild", "right": "severe" },
"rotationMidPain": { "left": "moderate", "right": "moderate" },
"rotationMaxPain": { "left": "moderate", "right": "severe" },
"pressElbowPain": { "left": "moderate", "right": "moderate" }
}
},
"risk": {
"SIS": { "percent": 28.999999999999996, "riskLevel": "MODERATE" },
"FS": { "percent": 24, "riskLevel": "LOW" }
}
}

Flattened (mskAssessResultFlattened) Example — Shoulder

{
"id": "523bOJ5ky183Qr5qZ61B",
"clientId": "bupa-uat",
"userId": "mzkQP97YwpdjpmWS7a9qZTB4w9t1",
"assessmentCode": "shoulder",
"createdAt._seconds": 1751949178,
"createdAt._nanoseconds": 31000000,
"assessmentSurvey.version": "SHOULDER_ASSESSMENT_SURVEY_CONTENT_V1",
"assessmentSurvey.shoulderPainRecurring": false,
"assessmentSurvey.gender": "male",
"assessmentSurvey.age": 45,
"assessmentResults.abduction.left": 180,
"assessmentResults.abduction.right": 180,
"painReview.abductionMaxPain.left": "mild",
"painReview.abductionMaxPain.right": "severe",
"risk.SIS.percent": 29,
"risk.SIS.riskLevel": "MODERATE",
"risk.FS.percent": 24,
"risk.FS.riskLevel": "LOW"
}
Flattened Format

The flattened format uses dot-notation keys and removes the assessmentData. prefix. The example above is abbreviated — actual payloads include all fields.

Field Reference — Shoulder

Assessment Survey fields (assessmentSurvey.):

KeyTypeDescription
version"SHOULDER_ASSESSMENT_SURVEY_CONTENT_V1"Schema version
agenumberUser age (18–100)
gender"male" | "female" | "undefined"User gender
diabetes"none" | "type1" | "type2"Diabetes status
shoulderPainRecurringbooleanRecurring shoulder pain
shoulderPainRelievedByRestbooleanPain relieved by rest
shoulderPainOverheadActivitybooleanPain during overhead activity
shoulderPainInModerateMotionbooleanPain in moderate motion
shoulderPainFixedPositionbooleanPain in fixed position
shoulderPainWithHandBehindBackbooleanPain with hand behind back
shoulderPainRepetitiveMotionbooleanPain with repetitive motion
shoulderPainWithOverheadActivitybooleanPain with overhead activity
shoulderPainAtLimitbooleanPain at limit of motion
shoulderPainNoInjurybooleanPain without injury
shoulderRangeOfMotionLimitedbooleanLimited range of motion

Assessment Results fields (assessmentResults.):

KeyTypeDescription
version"SHOULDER_ASSESSMENT_RESULTS_V1"Schema version
abduction.left, abduction.rightnumberAbduction angle (degrees)
flexion.left, flexion.rightnumberFlexion angle (degrees)
extension.left, extension.rightnumberExtension angle (degrees)
externalRotation.left, externalRotation.rightnumberExternal rotation angle
internalRotation.left, internalRotation.rightnumberInternal rotation angle
compression.left, compression.rightnumberCompression test result

Pain Review fields (painReview.):

KeyTypeDescription
version"SHOULDER_PAIN_REVIEW_CONTENT_V1"Schema version
flexionMidPain.left, flexionMidPain.right"none" | "mild" | "moderate" | "severe"Mid-flexion pain
flexionMaxPain.left, flexionMaxPain.right"none" | "mild" | "moderate" | "severe"Max flexion pain
abductionMidPain.left, abductionMidPain.right"none" | "mild" | "moderate" | "severe"Mid-abduction pain
abductionMaxPain.left, abductionMaxPain.right"none" | "mild" | "moderate" | "severe"Max abduction pain
rotationMidPain.left, rotationMidPain.right"none" | "mild" | "moderate" | "severe"Mid-rotation pain
rotationMaxPain.left, rotationMaxPain.right"none" | "mild" | "moderate" | "severe"Max rotation pain
pressElbowPain.left, pressElbowPain.right"none" | "mild" | "moderate" | "severe"Elbow press pain

Risk Assessment fields (risk.):

KeyDescription
SIS.percent, SIS.riskLevelShoulder Impingement Syndrome risk
FS.percent, FS.riskLevelFrozen Shoulder risk

Low-Back Assessment (low-back)

Nested (mskAssessResult) Example — Low-Back

{
"id": "abc123LowBackId",
"clientId": "bupa-uat",
"userId": "userXYZ123",
"assessmentCode": "low-back",
"createdAt": "2025-08-10T09:15:00.000Z",
"assessmentData": {
"assessmentSurvey": {
"version": "LOW_BACK_ASSESSMENT_SURVEY_CONTENT_V1",
"gender": "male",
"isPainFromSportsOrHandling": true,
"isPainWorseSittingOrStanding": true,
"hasSwellingOrWarmth": false,
"hasParaspinalMusclePain": true,
"hasRopeLikeLump": false,
"hasMorningStiffness": true,
"isPainWithProlongedSitting": true,
"isPainWithBackExtension": false,
"isPainWithProlongedWalking": true,
"isPainWithForwardBending": true,
"isPainWithKneesToChest": false,
"isPainWithCoughingSneezing": false,
"isPainRelievedByProneLying": true
},
"assessmentResults": {
"version": "LOW_BACK_ASSESSMENT_RESULTS_V1",
"seatedForwardFlexion": {
"normalizedFingerToToeDistance": -0.15
}
},
"painReview": {
"version": "LOW_BACK_PAIN_REVIEW_CONTENT_V1",
"painLevel": { "left": "moderate", "right": "mild" },
"painTiming": "midrange",
"radiatingPainTiming": "none"
}
},
"risk": {
"LB": { "percent": 45, "riskLevel": "MODERATE" },
"LDC": { "percent": 30, "riskLevel": "LOW" },
"LDH": { "percent": 25, "riskLevel": "LOW" }
}
}

Field Reference — Low-Back

Assessment Survey fields (assessmentSurvey.):

KeyTypeDescription
version"LOW_BACK_ASSESSMENT_SURVEY_CONTENT_V1"Schema version
gender"male" | "female" | "undefined"User gender
isPainFromSportsOrHandlingbooleanPain from sports or heavy lifting
isPainWorseSittingOrStandingbooleanPain worsens when sitting/standing
hasSwellingOrWarmthbooleanSwelling or warmth present
hasParaspinalMusclePainbooleanParaspinal muscle pain
hasRopeLikeLumpbooleanRope-like lump present
hasMorningStiffnessbooleanMorning stiffness
isPainWithProlongedSittingbooleanPain with prolonged sitting
isPainWithBackExtensionbooleanPain with back extension
isPainWithProlongedWalkingbooleanPain with prolonged walking
isPainWithForwardBendingbooleanPain with forward bending
isPainWithKneesToChestbooleanPain with knees to chest
isPainWithCoughingSneezingbooleanPain with coughing/sneezing
isPainRelievedByProneLyingbooleanPain relieved by prone lying

Assessment Results fields (assessmentResults.):

KeyTypeDescription
version"LOW_BACK_ASSESSMENT_RESULTS_V1"Schema version
seatedForwardFlexion.normalizedFingerToToeDistancenumberFinger-to-toe distance normalized by leg length.
0 = touching toes, negative = not reaching, positive = beyond toes

Pain Review fields (painReview.):

KeyTypeDescription
version"LOW_BACK_PAIN_REVIEW_CONTENT_V1"Schema version
painLevel.left"none" | "mild" | "moderate" | "severe"Left back pain level
painLevel.right"none" | "mild" | "moderate" | "severe"Right back pain level
painTiming"none" | "midrange" | "endpoint" | "constant"When pain occurs during test
radiatingPainTiming"none" | "early" | "midrange" | "endpoint"When radiating pain occurs

Risk Assessment fields (risk.):

KeyDescription
LB.percent, LB.riskLevelLow Back Muscle Strain risk
LDC.percent, LDC.riskLevelLumbar Degenerative Compression risk
LDH.percent, LDH.riskLevelLumbar Disc Herniation risk

Knee Assessment (knee)

Nested (mskAssessResult) Example — Knee

{
"id": "knee456ExampleId",
"clientId": "bupa-uat",
"userId": "userABC789",
"assessmentCode": "knee",
"createdAt": "2025-08-12T14:30:00.000Z",
"assessmentData": {
"assessmentSurvey": {
"version": "KNEE_ASSESSMENT_SURVEY_CONTENT_V1",
"ageOver35": true,
"gender": "female",
"isOverweight": false,
"hasKneeInjuryOrSurgery": false,
"hasMorningKneeStiffnessOrPain": true,
"hasProlongedSittingDiscomfort": true,
"hasKneePainOnWalkingStairsOrSquat": true,
"hasAnteriorKneePainOnFlexion": false,
"hasKneePainOnStandingUp": true,
"hasKneeLockingOrInstability": false,
"hasKneePainLimitingDailyTasks": false
},
"assessmentResults": {
"version": "KNEE_ASSESSMENT_RESULTS_V1",
"squat": {
"angle": 95,
"valgus": { "left": 5, "right": 8 }
}
},
"painReview": {
"version": "KNEE_PAIN_REVIEW_CONTENT_V1",
"painLevel": { "left": "mild", "right": "moderate" },
"painTiming": "endpoint",
"hasMildDiscomfortNearFullDepth": true,
"hasSharpAnteriorKneePainMidRange": false
}
},
"risk": {
"OA": { "percent": 35, "riskLevel": "MODERATE" },
"PFPS": { "percent": 20, "riskLevel": "LOW" }
}
}

Field Reference — Knee

Assessment Survey fields (assessmentSurvey.):

KeyTypeDescription
version"KNEE_ASSESSMENT_SURVEY_CONTENT_V1"Schema version
ageOver35boolean (optional)User is over 35 years old
gender"male" | "female" | "undefined"User gender
isOverweightbooleanBMI ≥ 25
hasKneeInjuryOrSurgerybooleanPrevious knee injury or surgery
hasMorningKneeStiffnessOrPainbooleanMorning stiffness or pain
hasProlongedSittingDiscomfortbooleanDiscomfort after prolonged sitting
hasKneePainOnWalkingStairsOrSquatbooleanPain when walking stairs or squatting
hasAnteriorKneePainOnFlexionbooleanAnterior knee pain on flexion
hasKneePainOnStandingUpbooleanPain when standing up
hasKneeLockingOrInstabilitybooleanKnee locking or instability
hasKneePainLimitingDailyTasksbooleanPain limits daily activities

Assessment Results fields (assessmentResults.):

KeyTypeDescription
version"KNEE_ASSESSMENT_RESULTS_V1"Schema version
squat.anglenumber (0–180)Squat depth angle in degrees
squat.valgus.leftnumberLeft knee valgus (inward bending) measurement
squat.valgus.rightnumberRight knee valgus (inward bending) measurement

Pain Review fields (painReview.):

KeyTypeDescription
version"KNEE_PAIN_REVIEW_CONTENT_V1"Schema version
painLevel.left"none" | "mild" | "moderate" | "severe"Left knee pain level
painLevel.right"none" | "mild" | "moderate" | "severe"Right knee pain level
painTiming"none" | "midrange" | "endpoint" | "constant"When pain occurs during squat
hasMildDiscomfortNearFullDepthbooleanMild discomfort only at full squat depth
hasSharpAnteriorKneePainMidRangebooleanSharp anterior knee pain at mid-range

Risk Assessment fields (risk.):

KeyDescription
OA.percent, OA.riskLevelKnee Osteoarthritis risk
PFPS.percent, PFPS.riskLevelPatellofemoral Pain Syndrome risk

Event Details

START_MSK_ASSESSMENT

When: User initiates a new MSK assessment Use Cases: Track assessment start, initialize local state, show progress indicator

// Data structure:
{
'assessmentResultId': 'uuid-string',
'assessmentCode': 'string',
'createdAt': 'iso-date-string',
'userId': 'string-or-null',
'mskAssessResult': { /* nested/original result (may be partial early on) */ },
'mskAssessResultFlattened': { /* flattened snapshot (may be partial) */ }
}

FINISH_ASSESSMENT_SURVEY

When: User completes the initial questionnaire Use Cases: Update progress, save intermediate state, track completion rate

// Data structure:
{
'assessmentResultId': 'uuid-string',
'mskAssessResult': { /* nested/original */ },
'mskAssessResultFlattened': { /* flattened */ }
}

FINISH_PHYSICAL_ASSESSMENT

When: User completes all physical tests Use Cases: Update progress, prepare for pain review phase

// Data structure:
{
'assessmentResultId': 'uuid-string',
'mskAssessResult': { /* nested/original */ },
'mskAssessResultFlattened': { /* flattened */ }
}

FINISH_PAIN_REVIEW

When: User completes pain location and intensity mapping Use Cases: Update progress, prepare for report generation

// Data structure:
{
'assessmentResultId': 'uuid-string',
'mskAssessResult': { /* nested/original */ },
'mskAssessResultFlattened': { /* flattened */ }
}

GENERATED_REPORT

When: System completes assessment processing and generates final report Use Cases: Notify user, save report data, display or download PDF, show completion

// Data structure:
{
'assessmentResultId': 'uuid-string',
'reportUrl': 'data:application/pdf;base64,...', // Base64 data URL of the PDF
'mskAssessResult': { /* nested/original */ },
'mskAssessResultFlattened': { /* flattened */ }
}

Handling the PDF Data URL

The reportUrl is provided as a base64 data URL that you can use directly in your Flutter app:

void _onReportGenerated(Map<String, dynamic> data) {
final String reportUrl = data['reportUrl']; // data:application/pdf;base64,...

// Extract base64 data
final String base64Data = reportUrl.split(',')[1];
final Uint8List pdfBytes = base64Decode(base64Data);

// Save to device storage
_savePdfToFile(pdfBytes, 'msk_report_${data['assessmentResultId']}.pdf');

// Or display in a PDF viewer
_showPdfViewer(pdfBytes);
}

Future<void> _savePdfToFile(Uint8List pdfBytes, String filename) async {
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/$filename');
await file.writeAsBytes(pdfBytes);
print('PDF saved to: ${file.path}');
}

EARLY_QUIT_ASSESSMENT

When: User exits assessment before completion Use Cases: Handle incomplete state, show retention messaging, track dropout reasons

// Data structure:
{
'assessmentResultId': 'uuid-string-or-null',
'reason': 'string-or-null',
'mskAssessResult': { /* optional partial */ },
'mskAssessResultFlattened': { /* optional partial */ }
}

QUIT_MSK_MODULE

When: User exits the entire MSK module Use Cases: Navigate back, clear state, track module engagement

// Data structure:
{
'mskAssessResult': { /* optional */ },
'mskAssessResultFlattened': { /* optional */ }
}