maintainx-migration-deep-dive
Execute complete platform migrations to or from MaintainX. Use when migrating from legacy CMMS systems, performing major re-platforming, or transitioning to MaintainX from spreadsheets or other tools. Trigger with phrases like "migrate to maintainx", "maintainx migration", "cmms migration", "switch to maintainx", "maintainx data migration". allowed-tools: Read, Write, Edit, Bash(npm:*), Bash(node:*) version: 1.0.0 license: MIT author: Jeremy Longshore <jeremy@intentsolutions.io>
Allowed Tools
No tools specified
Provided by Plugin
maintainx-pack
Claude Code skill pack for MaintainX CMMS (24 skills)
Installation
This skill is included in the maintainx-pack plugin:
/plugin install maintainx-pack@claude-code-plugins-plus
Click to copy
Instructions
# MaintainX Migration Deep Dive
## Overview
Comprehensive guide for migrating to MaintainX from legacy CMMS systems, spreadsheets, or other maintenance management tools.
## Prerequisites
- MaintainX account with API access
- Access to source system data
- Understanding of both systems' data models
## Migration Strategy
```
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Migration Strategy Overview β
β β
β Phase 1 Phase 2 Phase 3 Phase 4 β
β ASSESS PREPARE MIGRATE VALIDATE β
β β
β ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ β
β βInventory β βData β βParallel β βTesting β β
β βdata βββββΆβcleansing βββββΆβrun βββββΆβ& cutover β β
β β β β& mapping β β β β β β
β ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ β
β β
β Week 1-2 Week 3-4 Week 5-6 Week 7-8 β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
## Instructions
### Step 1: Source System Assessment
```typescript
// scripts/migration-assessment.ts
interface SourceSystemAssessment {
system: string;
dataVolume: {
workOrders: number;
assets: number;
locations: number;
users: number;
procedures: number;
};
dataQuality: {
completeness: number; // Percentage
duplicates: number;
invalidRecords: number;
};
customizations: string[];
integrations: string[];
risks: string[];
}
async function assessSourceSystem(sourceData: any): Promise {
console.log('=== Migration Assessment ===\n');
const assessment: SourceSystemAssessment = {
system: sourceData.systemName || 'Unknown',
dataVolume: {
workOrders: 0,
assets: 0,
locations: 0,
users: 0,
procedures: 0,
},
dataQuality: {
completeness: 0,
duplicates: 0,
invalidRecords: 0,
},
customizations: [],
integrations: [],
risks: [],
};
// Count records
console.log('Counting records...');
assessment.dataVolume.workOrders = sourceData.workOrders?.length || 0;
assessment.dataVolume.assets = sourceData.assets?.length || 0;
assessment.dataVolume.locations = sourceData.locations?.length || 0;
assessment.dataVolume.users = sourceData.users?.length || 0;
assessment.dataVolume.procedures = sourceData.procedures?.length || 0;
// Analyze data quality
console.log('Analyzing data quality...');
const workOrders = sourceData.workOrders || [];
// Check completeness (required fields)
const requiredFields = ['title', 'createdAt'];
let completeRecords = 0;
workOrders.forEach((wo: any) => {
const hasAllRequired = requiredFields.every(f => wo[f]);
if (hasAllRequired) completeRecords++;
});
assessment.dataQuality.completeness = workOrders.length > 0
? (completeRecords / workOrders.length) * 100
: 100;
// Check for duplicates
const titles = workOrders.map((wo: any) => wo.title);
const uniqueTitles = new Set(titles);
assessment.dataQuality.duplicates = titles.length - uniqueTitles.size;
// Identify risks
if (assessment.dataVolume.workOrders > 10000) {
assessment.risks.push('Large data volume may require batched migration');
}
if (assessment.dataQuality.completeness < 80) {
assessment.risks.push('Low data completeness - may need data enrichment');
}
if (assessment.dataQuality.duplicates > 100) {
assessment.risks.push('Significant duplicates - deduplication recommended');
}
return assessment;
}
// Print assessment report
function printAssessmentReport(assessment: SourceSystemAssessment): void {
console.log('\n=== Assessment Report ===\n');
console.log('Data Volume:');
console.log(` Work Orders: ${assessment.dataVolume.workOrders.toLocaleString()}`);
console.log(` Assets: ${assessment.dataVolume.assets.toLocaleString()}`);
console.log(` Locations: ${assessment.dataVolume.locations.toLocaleString()}`);
console.log(` Users: ${assessment.dataVolume.users.toLocaleString()}`);
console.log(` Procedures: ${assessment.dataVolume.procedures.toLocaleString()}`);
console.log('\nData Quality:');
console.log(` Completeness: ${assessment.dataQuality.completeness.toFixed(1)}%`);
console.log(` Duplicates: ${assessment.dataQuality.duplicates}`);
console.log(` Invalid Records: ${assessment.dataQuality.invalidRecords}`);
if (assessment.risks.length > 0) {
console.log('\nRisks:');
assessment.risks.forEach(r => console.log(` - ${r}`));
}
}
```
### Step 2: Data Mapping
```typescript
// src/migration/data-mapping.ts
interface FieldMapping {
sourceField: string;
targetField: string;
transform?: (value: any) => any;
required: boolean;
defaultValue?: any;
}
interface DataMapping {
workOrders: FieldMapping[];
assets: FieldMapping[];
locations: FieldMapping[];
users: FieldMapping[];
}
// Common mappings from legacy systems
const legacyCmmsToMaintainX: DataMapping = {
workOrders: [
{ sourceField: 'wo_number', targetField: 'externalId', required: false },
{ sourceField: 'wo_title', targetField: 'title', required: true },
{ sourceField: 'wo_description', targetField: 'description', required: false },
{
sourceField: 'wo_priority',
targetField: 'priority',
required: false,
defaultValue: 'MEDIUM',
transform: (value) => {
const map: Record = {
'1': 'HIGH', 'high': 'HIGH', 'urgent': 'HIGH',
'2': 'MEDIUM', 'medium': 'MEDIUM', 'normal': 'MEDIUM',
'3': 'LOW', 'low': 'LOW', 'minor': 'LOW',
};
return map[String(value).toLowerCase()] || 'MEDIUM';
},
},
{
sourceField: 'wo_status',
targetField: 'status',
required: false,
defaultValue: 'OPEN',
transform: (value) => {
const map: Record = {
'new': 'OPEN', 'open': 'OPEN', 'pending': 'OPEN',
'in_progress': 'IN_PROGRESS', 'active': 'IN_PROGRESS', 'working': 'IN_PROGRESS',
'hold': 'ON_HOLD', 'waiting': 'ON_HOLD', 'paused': 'ON_HOLD',
'complete': 'DONE', 'completed': 'DONE', 'closed': 'DONE', 'done': 'DONE',
};
return map[String(value).toLowerCase()] || 'OPEN';
},
},
{ sourceField: 'created_date', targetField: 'createdAt', required: false },
{ sourceField: 'due_date', targetField: 'dueDate', required: false },
{ sourceField: 'equipment_id', targetField: 'assetId', required: false },
{ sourceField: 'location_id', targetField: 'locationId', required: false },
],
assets: [
{ sourceField: 'asset_id', targetField: 'externalId', required: false },
{ sourceField: 'asset_name', targetField: 'name', required: true },
{ sourceField: 'serial_number', targetField: 'serialNumber', required: false },
{ sourceField: 'model_number', targetField: 'model', required: false },
{ sourceField: 'manufacturer', targetField: 'manufacturer', required: false },
{
sourceField: 'asset_status',
targetField: 'status',
required: false,
defaultValue: 'OPERATIONAL',
transform: (value) => {
const map: Record = {
'active': 'OPERATIONAL', 'operational': 'OPERATIONAL', 'running': 'OPERATIONAL',
'down': 'NON_OPERATIONAL', 'broken': 'NON_OPERATIONAL', 'offline': 'NON_OPERATIONAL',
'retired': 'DECOMMISSIONED', 'decommissioned': 'DECOMMISSIONED',
};
return map[String(value).toLowerCase()] || 'OPERATIONAL';
},
},
],
locations: [
{ sourceField: 'location_id', targetField: 'externalId', required: false },
{ sourceField: 'location_name', targetField: 'name', required: true },
{ sourceField: 'address', targetField: 'address', required: false },
{ sourceField: 'parent_location_id', targetField: 'parentId', required: false },
],
users: [
{ sourceField: 'user_id', targetField: 'externalId', required: false },
{ sourceField: 'first_name', targetField: 'firstName', required: true },
{ sourceField: 'last_name', targetField: 'lastName', required: true },
{ sourceField: 'email', targetField: 'email', required: true },
],
};
// Apply mapping to transform data
function applyMapping(
sourceRecords: any[],
mappings: FieldMapping[]
): T[] {
return sourceRecords.map(source => {
const target: any = {};
mappings.forEach(mapping => {
let value = source[mapping.sourceField];
// Apply transform if defined
if (mapping.transform && value !== undefined && value !== null) {
value = mapping.transform(value);
}
// Apply default if value is missing
if ((value === undefined || value === null) && mapping.defaultValue !== undefined) {
value = mapping.defaultValue;
}
// Skip if no value and not required
if (value === undefined || value === null) {
if (mapping.required) {
throw new Error(`Required field missing: ${mapping.sourceField}`);
}
return;
}
target[mapping.targetField] = value;
});
return target as T;
});
}
```
### Step 3: Migration Execution
```typescript
// src/migration/migrator.ts
interface MigrationPlan {
phases: MigrationPhase[];
rollbackPlan: RollbackStep[];
validationRules: ValidationRule[];
}
interface MigrationPhase {
name: string;
order: number;
resource: 'locations' | 'assets' | 'users' | 'workOrders';
batchSize: number;
dependsOn?: string[];
}
interface MigrationResult {
phase: string;
status: 'success' | 'partial' | 'failed';
processed: number;
succeeded: number;
failed: number;
errors: MigrationError[];
duration: number;
}
class MaintainXMigrator {
private client: MaintainXClient;
private idMapping: Map = new Map(); // sourceId -> maintainxId
async executeMigration(
sourceData: any,
plan: MigrationPlan
): Promise {
const results: MigrationResult[] = [];
// Sort phases by order and dependencies
const sortedPhases = this.sortPhases(plan.phases);
for (const phase of sortedPhases) {
console.log(`\n=== Executing Phase: ${phase.name} ===\n`);
const result = await this.executePhase(
phase,
sourceData[phase.resource],
plan.validationRules
);
results.push(result);
// Stop if critical phase failed
if (result.status === 'failed') {
console.error(`Phase ${phase.name} failed. Migration halted.`);
break;
}
}
return results;
}
private async executePhase(
phase: MigrationPhase,
sourceRecords: any[],
validationRules: ValidationRule[]
): Promise {
const startTime = Date.now();
const result: MigrationResult = {
phase: phase.name,
status: 'success',
processed: 0,
succeeded: 0,
failed: 0,
errors: [],
duration: 0,
};
// Transform data
const mappings = legacyCmmsToMaintainX[phase.resource];
let transformedRecords: any[];
try {
transformedRecords = applyMapping(sourceRecords, mappings);
} catch (error: any) {
result.status = 'failed';
result.errors.push({ record: null, error: error.message });
result.duration = Date.now() - startTime;
return result;
}
// Process in batches
for (let i = 0; i < transformedRecords.length; i += phase.batchSize) {
const batch = transformedRecords.slice(i, i + phase.batchSize);
for (const record of batch) {
result.processed++;
try {
// Resolve ID references
if (record.assetId && this.idMapping.has(record.assetId)) {
record.assetId = this.idMapping.get(record.assetId);
}
if (record.locationId && this.idMapping.has(record.locationId)) {
record.locationId = this.idMapping.get(record.locationId);
}
if (record.parentId && this.idMapping.has(record.parentId)) {
record.parentId = this.idMapping.get(record.parentId);
}
// Create in MaintainX
const created = await this.createRecord(phase.resource, record);
// Store ID mapping for references
if (record.externalId) {
this.idMapping.set(record.externalId, created.id);
}
result.succeeded++;
} catch (error: any) {
result.failed++;
result.errors.push({
record,
error: error.message,
});
}
}
console.log(`Progress: ${result.processed}/${transformedRecords.length}`);
// Rate limit protection
await new Promise(r => setTimeout(r, 100));
}
result.status = result.failed === 0 ? 'success' :
result.succeeded > 0 ? 'partial' : 'failed';
result.duration = Date.now() - startTime;
return result;
}
private async createRecord(resource: string, data: any): Promise {
switch (resource) {
case 'workOrders':
return this.client.createWorkOrder(data);
// Add other resources as API supports
default:
throw new Error(`Unsupported resource: ${resource}`);
}
}
}
```
### Step 4: Validation & Verification
```typescript
// src/migration/validation.ts
interface ValidationReport {
phase: string;
totalRecords: number;
passed: number;
failed: number;
failures: ValidationFailure[];
}
interface ValidationFailure {
sourceId: string;
targetId?: string;
issues: string[];
}
class MigrationValidator {
async validateMigration(
sourceData: any[],
targetData: any[]
): Promise {
const report: ValidationReport = {
phase: 'post-migration',
totalRecords: sourceData.length,
passed: 0,
failed: 0,
failures: [],
};
// Create lookup maps
const sourceMap = new Map(sourceData.map(s => [s.externalId || s.id, s]));
const targetMap = new Map(targetData.map(t => [t.externalId, t]));
// Validate each source record exists in target
for (const [sourceId, source] of sourceMap) {
const target = targetMap.get(sourceId);
const issues: string[] = [];
if (!target) {
issues.push('Record not found in target system');
} else {
// Validate critical fields match
if (source.title !== target.title) {
issues.push(`Title mismatch: "${source.title}" vs "${target.title}"`);
}
// Add more field validations as needed
}
if (issues.length === 0) {
report.passed++;
} else {
report.failed++;
report.failures.push({
sourceId,
targetId: target?.id,
issues,
});
}
}
return report;
}
async generateReconciliationReport(
sourceData: any[],
maintainxClient: MaintainXClient
): Promise {
console.log('=== Migration Reconciliation Report ===\n');
// Get counts from MaintainX
const woResponse = await maintainxClient.getWorkOrders({ limit: 1 });
const assetResponse = await maintainxClient.getAssets({ limit: 1 });
const locationResponse = await maintainxClient.getLocations({ limit: 1 });
console.log('Record Counts:');
console.log(` Source work orders: ${sourceData.workOrders?.length || 0}`);
console.log(` MaintainX work orders: ${woResponse.workOrders.length}+`);
console.log('');
console.log(` Source assets: ${sourceData.assets?.length || 0}`);
console.log(` MaintainX assets: ${assetResponse.assets.length}+`);
console.log('');
console.log(` Source locations: ${sourceData.locations?.length || 0}`);
console.log(` MaintainX locations: ${locationResponse.locations.length}+`);
}
}
```
### Step 5: Rollback Procedures
```typescript
// src/migration/rollback.ts
interface RollbackPlan {
steps: RollbackStep[];
checkpoint: Date;
}
interface RollbackStep {
order: number;
description: string;
execute: () => Promise;
}
class MigrationRollback {
private client: MaintainXClient;
private migratedIds: Map = new Map(); // resource -> ids
// Track migrated records for potential rollback
trackMigrated(resource: string, id: string): void {
if (!this.migratedIds.has(resource)) {
this.migratedIds.set(resource, []);
}
this.migratedIds.get(resource)!.push(id);
}
// Generate rollback plan
generateRollbackPlan(): RollbackPlan {
const plan: RollbackPlan = {
steps: [],
checkpoint: new Date(),
};
// Note: Actual rollback depends on available API endpoints
// MaintainX may not support programmatic deletion
plan.steps.push({
order: 1,
description: 'Document migrated record IDs',
execute: async () => {
const report = {
timestamp: new Date().toISOString(),
records: Object.fromEntries(this.migratedIds),
};
await fs.writeFile(
`rollback-data-${Date.now()}.json`,
JSON.stringify(report, null, 2)
);
console.log('Rollback data saved');
},
});
plan.steps.push({
order: 2,
description: 'Contact MaintainX support for bulk deletion',
execute: async () => {
console.log('Manual step required:');
console.log('1. Contact MaintainX support');
console.log('2. Provide list of migrated record IDs');
console.log('3. Request bulk deletion or archival');
},
});
plan.steps.push({
order: 3,
description: 'Verify rollback completion',
execute: async () => {
// Verify records are removed
console.log('Verify records have been removed from MaintainX');
},
});
return plan;
}
async executeRollback(plan: RollbackPlan): Promise {
console.log('=== Executing Rollback ===\n');
for (const step of plan.steps.sort((a, b) => a.order - b.order)) {
console.log(`Step ${step.order}: ${step.description}`);
await step.execute();
console.log('Done\n');
}
console.log('Rollback complete');
}
}
```
## Migration Checklist
- [ ] Source system assessed
- [ ] Data mapping defined
- [ ] Test migration completed (subset)
- [ ] Full migration executed
- [ ] Validation passed
- [ ] User acceptance testing
- [ ] Cutover completed
- [ ] Source system decommissioned
## Output
- Assessment report
- Data mapping configuration
- Migration results
- Validation report
- Rollback plan
## Timeline Template
| Week | Phase | Activities |
|------|-------|------------|
| 1-2 | Assessment | Data inventory, quality analysis |
| 3-4 | Preparation | Mapping, test migration |
| 5-6 | Migration | Parallel run, data migration |
| 7-8 | Validation | Testing, cutover, decommission |
## Resources
- [MaintainX API Documentation](https://maintainx.dev/)
- [MaintainX Import Guide](https://help.getmaintainx.com/)
- [Data Migration Best Practices](https://martinfowler.com/articles/data-migration-patterns.html)
## Next Steps
Congratulations! You have completed the MaintainX skill pack. For additional support, contact MaintainX at support@getmaintainx.com.