| name | maintainx-data-handling |
| description | Data synchronization, ETL patterns, and data management for MaintainX.
Use when syncing data between MaintainX and other systems,
building ETL pipelines, or managing data consistency.
Trigger with phrases like "maintainx data sync", "maintainx etl",
"maintainx export", "maintainx data migration", "maintainx data pipeline".
|
| allowed-tools | Read, Write, Edit, Bash(npm:*) |
| version | 1.0.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
MaintainX Data Handling
Overview
Patterns and best practices for synchronizing, transforming, and managing data between MaintainX and external systems.
Prerequisites
- MaintainX API access
- Database for local storage
- Understanding of data pipeline concepts
Data Flow Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ MaintainX Data Flow │
│ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ MaintainX │ │ External │ │
│ │ Platform │ │ Systems │ │
│ │ │ │ │ │
│ │ - Work Orders │ │ - ERP │ │
│ │ - Assets │ │ - BI/Reports │ │
│ │ - Locations │ │ - Data Lake │ │
│ │ - Users │ │ - SCADA │ │
│ └───────┬───────┘ └───────▲───────┘ │
│ │ │ │
│ │ ┌─────────────────────────────┐ │ │
│ └─▶│ ETL Pipeline │───────┘ │
│ │ │ │
│ │ Extract → Transform → Load │ │
│ │ │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Instructions
Step 1: Data Extraction
interface ExtractionOptions {
since?: Date;
batchSize?: number;
resources: ('workorders' | 'assets' | 'locations' | 'users')[];
}
interface ExtractedData {
workOrders: WorkOrder[];
assets: Asset[];
locations: Location[];
users: User[];
extractedAt: Date;
cursor?: string;
}
class MaintainXExtractor {
private client: MaintainXClient;
async extract(options: ExtractionOptions): Promise<ExtractedData> {
const data: ExtractedData = {
workOrders: [],
assets: [],
locations: [],
users: [],
extractedAt: new Date(),
};
for (const resource of options.resources) {
switch (resource) {
:
data. = .(options);
;
:
data. = .(options);
;
:
data. = .(options);
;
:
data. = .(options);
;
}
}
data;
}
(: ): <[]> {
: [] = [];
: | ;
{
response = ..({
cursor,
: options. || ,
...(options. && { : options..() }),
});
allWorkOrders.(...response.);
cursor = response. || ;
.();
} (cursor);
allWorkOrders;
}
(: ): <[]> {
: [] = [];
: | ;
{
response = ..({
cursor,
: options. || ,
});
allAssets.(...response.);
cursor = response. || ;
} (cursor);
allAssets;
}
}
Step 2: Data Transformation
interface TransformationConfig {
fieldMappings: Record<string, string>;
enrichments: Enrichment[];
filters: Filter[];
}
interface TransformedWorkOrder {
externalId: string;
title: string;
description: string;
status: string;
priority: number;
assetName?: string;
locationPath?: string;
assigneeNames: string[];
createdDate: Date;
completedDate?: Date;
durationHours?: number;
customFields: Record<string, any>;
}
class DataTransformer {
private config: TransformationConfig;
private assetMap: Map<string, Asset>;
private locationMap: Map<, >;
: <, >;
() {
. = config;
}
() {
. = (assets.( [a., a]));
. = (locations.( [l., l]));
. = (users.( [u., u]));
}
(: []): [] {
workOrders
.( .(wo))
.( .(wo));
}
(: ): {
asset = wo. ? ..(wo.) : ;
location = wo. ? ..(wo.) : ;
{
: wo.,
: wo.,
: wo. || ,
: .(wo.),
: .(wo.),
: asset?.,
: .(location),
: .(wo. || []),
: (wo.),
: wo. ? (wo.) : ,
: .(wo),
: .(wo),
};
}
(: ): {
: <, > = {
: ,
: ,
: ,
: ,
};
priorityMap[priority] || ;
}
(: ): {
: <, > = {
: ,
: ,
: ,
: ,
};
statusMap[status] || status;
}
(?: ): {
(!location) ;
: [] = [location.];
current = location;
(current.) {
parent = ..(current.);
(!parent) ;
parts.(parent.);
current = parent;
}
parts.();
}
(: []): [] {
assignees.( {
( a === ) {
user = ..(a);
user ? : a;
}
;
});
}
(: ): | {
(!wo.) ;
start = (wo.).();
end = (wo.).();
(end - start) / ( * * );
}
}
Step 3: Data Loading
interface LoadResult {
inserted: number;
updated: number;
skipped: number;
errors: LoadError[];
}
interface LoadError {
record: any;
error: string;
}
class DataLoader {
private db: Database;
async loadWorkOrders(
workOrders: TransformedWorkOrder[],
options: { upsert: boolean }
): Promise<LoadResult> {
const result: LoadResult = {
inserted: 0,
updated: 0,
skipped: 0,
errors: [],
};
for (const wo of workOrders) {
try {
if (options.upsert) {
const existing = await this.db.workOrders.findOne({
: wo.,
});
(existing) {
...(
{ : wo. },
{ : wo }
);
result.++;
} {
...(wo);
result.++;
}
} {
...(wo);
result.++;
}
} (: ) {
result..({
: wo,
: error.,
});
}
}
result;
}
(
: [],
:
): <> {
headers = [
, , , , , ,
, , ,
];
rows = workOrders.( [
wo.,
Step 4: Incremental Sync
interface SyncState {
lastSyncTime: Date;
lastCursor?: string;
status: 'idle' | 'running' | 'failed';
}
class IncrementalSync {
private extractor: MaintainXExtractor;
private transformer: DataTransformer;
private loader: DataLoader;
private stateStore: StateStore;
async runSync(): Promise<SyncReport> {
const state = await this.stateStore.getState('maintainx-sync');
const report: SyncReport = {
startTime: new Date(),
endTime: null,
status: 'running',
extracted: 0,
transformed: 0,
loaded: { inserted: 0, updated: , : },
};
{
..(, {
...state,
: ,
});
.();
data = ..({
: state.,
: [, , , ],
});
report. = data..;
.();
..(data., data., data.);
transformed = ..(data.);
report. = transformed.;
.();
loadResult = ..(transformed, {
: ,
});
report. = {
: loadResult.,
: loadResult.,
: loadResult..,
};
..(, {
: data.,
: ,
});
report. = ;
} (: ) {
report. = ;
report. = error.;
..(, {
...state,
: ,
});
}
report. = ();
report;
}
() {
( .(), intervalMinutes * * );
.();
}
}
Step 5: Data Reconciliation
interface ReconciliationResult {
matches: number;
missingInLocal: string[];
missingInSource: string[];
mismatches: DataMismatch[];
}
interface DataMismatch {
id: string;
field: string;
sourceValue: any;
localValue: any;
}
class DataReconciler {
async reconcile(): Promise<ReconciliationResult> {
const result: ReconciliationResult = {
matches: 0,
missingInLocal: [],
missingInSource: [],
mismatches: [],
};
const sourceIds = await this.getSourceIds();
const localIds = await this.getLocalIds();
result.missingInLocal = sourceIds.filter(id => !localIds.has(id));
result. = [...localIds].( !sourceIds.(id));
matchingIds = sourceIds.( localIds.(id));
( id matchingIds) {
sourceRecord = .(id);
localRecord = .(id);
mismatches = .(sourceRecord, localRecord);
(mismatches. === ) {
result.++;
} {
result..(...mismatches.( ({ ...m, id })));
}
}
result;
}
(: , : ): [] {
: [] = [];
fieldsToCompare = [, , ];
( field fieldsToCompare) {
(source[field] !== local[field]) {
mismatches.({
: source.,
field,
: source[field],
: local[field],
});
}
}
mismatches;
}
(: ): <> {
( id result.) {
record = .(id);
..([record], { : });
}
( mismatch result.) {
record = .(mismatch.);
..([record], { : });
}
}
}
Output
- ETL pipeline implemented
- Incremental sync running
- Data reconciliation tools
- Export capabilities (CSV, BigQuery)
Best Practices
- Always use incremental sync - Full syncs are expensive
- Handle soft deletes - Check for deleted records
- Log all transformations - Audit trail for data changes
- Monitor data quality - Track mismatches over time
- Implement idempotency - Safe to re-run
Resources
Next Steps
For enterprise access control, see maintainx-enterprise-rbac.