agentsclimarketplace

Maintainx sdk patterns

Skill jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/maintainx-pack/skills/maintainx-sdk-patterns

'Learn MaintainX REST API patterns, pagination, filtering, and client architecture.From its SKILL.md

Install
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill maintainx-sdk-patterns

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its file declares

Copied from the file, not written here

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

8.5 KB, ~2.1k tokens by cl100k_base, as published. Nobody here has run it

MaintainX SDK Patterns

Overview

Production-grade patterns for building robust MaintainX API integrations with proper error handling, cursor-based pagination, retry logic, and type safety.

Prerequisites

  • Completed maintainx-install-auth setup
  • TypeScript/Node.js familiarity
  • Understanding of REST API principles

Instructions

Step 1: Type-Safe Client with Generics

// src/maintainx/typed-client.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios';

interface PaginatedResponse<T> {
  cursor: string | null;
}

interface WorkOrder {
  id: number;
  title: string;
  status: 'OPEN' | 'IN_PROGRESS' | 'ON_HOLD' | 'COMPLETED' | 'CLOSED';
  priority: 'NONE' | 'LOW' | 'MEDIUM' | 'HIGH';
  description?: string;
  assignees: Array<{ type: 'USER' | 'TEAM'; id: number }>;
  assetId?: number;
  locationId?: number;
  createdAt: string;
  updatedAt: string;
  completedAt?: string;
  dueDate?: string;
  categories: string[];
}

interface Asset {
  id: number;
  name: string;
  serialNumber?: string;
  model?: string;
  manufacturer?: string;
  locationId?: number;
  createdAt: string;
}

interface WorkOrdersResponse extends PaginatedResponse<WorkOrder> {
  workOrders: WorkOrder[];
}

interface AssetsResponse extends PaginatedResponse<Asset> {
  assets: Asset[];
}

export class MaintainXClient {
  private http: AxiosInstance;

  constructor(apiKey?: string) {
    const key = apiKey || process.env.MAINTAINX_API_KEY;
    if (!key) throw new Error('MAINTAINX_API_KEY required');

    this.http = axios.create({
      baseURL: 'https://api.getmaintainx.com/v1',
      headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
      timeout: 30_000,
    });
  }

  async getWorkOrders(params?: Record<string, any>): Promise<WorkOrdersResponse> {
    const { data } = await this.http.get<WorkOrdersResponse>('/workorders', { params });
    return data;
  }

  async getWorkOrder(id: number): Promise<WorkOrder> {
    const { data } = await this.http.get<WorkOrder>(`/workorders/${id}`);
    return data;
  }

  async createWorkOrder(input: Partial<WorkOrder>): Promise<WorkOrder> {
    const { data } = await this.http.post<WorkOrder>('/workorders', input);
    return data;
  }

  async updateWorkOrder(id: number, input: Partial<WorkOrder>): Promise<WorkOrder> {
    const { data } = await this.http.patch<WorkOrder>(`/workorders/${id}`, input);
    return data;
  }

  async getAssets(params?: Record<string, any>): Promise<AssetsResponse> {
    const { data } = await this.http.get<AssetsResponse>('/assets', { params });
    return data;
  }

  async request<T = any>(method: string, path: string, body?: any): Promise<T> {
    const config: AxiosRequestConfig = { method, url: path, data: body };
    const { data } = await this.http.request<T>(config);
    return data;
  }
}

Step 2: Cursor-Based Pagination

MaintainX uses cursor-based pagination. The response includes a cursor field; pass it as a query parameter to get the next page.

async function paginate<T>(
  fetcher: (cursor?: string) => Promise<{ cursor: string | null } & Record<string, T[]>>,
  key: string,
): Promise<T[]> {
  const all: T[] = [];
  let cursor: string | undefined;

  do {
    const response = await fetcher(cursor);
    const items = (response as any)[key] as T[];
    all.push(...items);
    cursor = response.cursor ?? undefined;
  } while (cursor);

  return all;
}

// Usage
const allWorkOrders = await paginate(
  (cursor) => client.getWorkOrders({ limit: 100, cursor, status: 'OPEN' }),
  'workOrders',
);
console.log(`Total open work orders: ${allWorkOrders.length}`);

const allAssets = await paginate(
  (cursor) => client.getAssets({ limit: 100, cursor }),
  'assets',
);
console.log(`Total assets: ${allAssets.length}`);

Step 3: Retry with Exponential Backoff

async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries = 3,
  baseDelayMs = 1000,
): Promise<T> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (err: any) {
      const status = err?.response?.status;
      const isRetryable = status === 429 || (status >= 500 && status < 600);

      if (!isRetryable || attempt === maxRetries) throw err;

      // Honor Retry-After header if present
      const retryAfter = err.response?.headers?.['retry-after'];
      const delayMs = retryAfter
        ? parseInt(retryAfter) * 1000
        : baseDelayMs * Math.pow(2, attempt) + Math.random() * 500;

      console.warn(`Retry ${attempt + 1}/${maxRetries} after ${delayMs}ms (HTTP ${status})`);
      await new Promise((r) => setTimeout(r, delayMs));
    }
  }
  throw new Error('Unreachable');
}

// Usage
const wo = await withRetry(() => client.getWorkOrder(12345));

Step 4: Batch Operations

import PQueue from 'p-queue';

const queue = new PQueue({ concurrency: 5, interval: 1000, intervalCap: 10 });

async function batchCreateWorkOrders(items: Array<Partial<WorkOrder>>): Promise<WorkOrder[]> {
  const results = await Promise.all(
    items.map((item) =>
      queue.add(() => withRetry(() => client.createWorkOrder(item)))
    ),
  );
  return results as WorkOrder[];
}

// Create 50 PMs in controlled batches
const pms = Array.from({ length: 50 }, (_, i) => ({
  title: `Weekly Inspection - Zone ${i + 1}`,
  priority: 'LOW' as const,
  categories: ['PREVENTIVE'],
}));

const created = await batchCreateWorkOrders(pms);
console.log(`Created ${created.length} preventive maintenance orders`);

Step 5: Fluent Query Builder

class WorkOrderQuery {
  private params: Record<string, any> = {};

  status(s: WorkOrder['status']) { this.params.status = s; return this; }
  priority(p: WorkOrder['priority']) { this.params.priority = p; return this; }
  assignee(userId: number) { this.params.assigneeId = userId; return this; }
  asset(assetId: number) { this.params.assetId = assetId; return this; }
  location(locationId: number) { this.params.locationId = locationId; return this; }
  createdAfter(date: string) { this.params.createdAtGte = date; return this; }
  createdBefore(date: string) { this.params.createdAtLte = date; return this; }
  limit(n: number) { this.params.limit = n; return this; }

  async execute(client: MaintainXClient) {
    return client.getWorkOrders(this.params);
  }
}

// Usage
const results = await new WorkOrderQuery()
  .status('OPEN')
  .priority('HIGH')
  .location(2345)
  .createdAfter('2026-01-01T00:00:00Z')
  .limit(25)
  .execute(client);

Output

  • Type-safe MaintainX client with full TypeScript interfaces
  • Cursor-based pagination utility that works across all list endpoints
  • Retry logic with exponential backoff and Retry-After header support
  • Rate-limited batch processor using p-queue
  • Fluent query builder for readable work order filters

Error Handling

PatternUse Case
withRetry()Transient errors (429, 5xx) with exponential backoff
paginate()Collecting all items from cursor-based endpoints
PQueueControlled concurrency to avoid rate limits
WorkOrderQueryType-safe filtering to prevent invalid API calls

Resources

  • MaintainX API Reference
  • p-queue -- Promise queue with concurrency control

Next Steps

For core workflows, see maintainx-core-workflow-a (Work Orders) and maintainx-core-workflow-b (Assets).

Examples

Stream large datasets with async iterators:

async function* streamWorkOrders(client: MaintainXClient, params?: Record<string, any>) {
  let cursor: string | undefined;
  do {
    const response = await client.getWorkOrders({ ...params, limit: 100, cursor });
    for (const wo of response.workOrders) {
      yield wo;
    }
    cursor = response.cursor ?? undefined;
  } while (cursor);
}

for await (const wo of streamWorkOrders(client, { status: 'COMPLETED' })) {
  console.log(`Processing completed WO #${wo.id}`);
}

What ships with it: 1 file

11.6 KB alongside SKILL.md

references/

Keep looking

Skills are one crate of 326,144. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.