Skip to content

IBM Maximo Connector

The MaximoConnector integrates Machina with IBM Maximo Manage (EAM), reading and creating maintenance data via the OSLC/JSON REST API.

Prerequisites

  • IBM Maximo Manage instance (7.6.0.2+ or Maximo Application Suite)
  • API key (recommended), or Basic/MAXAUTH credentials
  • Network access to the Maximo OSLC endpoints (/maximo/oslc/os/)

Installation

pip install machina-ai[cmms-rest]

Configuration

from machina.connectors import Maximo
from machina.connectors.cmms import ApiKeyHeaderAuth
from machina.domain.asset import AssetType

connector = Maximo(
    url="https://maximo.example.com",
    auth=ApiKeyHeaderAuth(header_name="apikey", value="your-api-key"),
    asset_type_map={
        "PUMPS": AssetType.ROTATING_EQUIPMENT,
        "VESSELS": AssetType.STATIC_EQUIPMENT,
        "INSTRUMENTS": AssetType.INSTRUMENT,
    },
)
await connector.connect()
from machina.connectors import Maximo
from machina.connectors.cmms import BasicAuth

connector = Maximo(
    url="https://maximo.example.com",
    auth=BasicAuth(username="maxadmin", password="secret"),
)
await connector.connect()
connectors:
  cmms:
    type: maximo
    primary: true
    settings:
      url: https://maximo.example.com
      auth:
        type: api_key
        header_name: apikey
        value: ${MAXIMO_API_KEY}

Capabilities

Capability Description
read_assets Read asset records (mxasset object structure)
read_work_orders Read work orders — filter by asset_id and/or status (accepts WorkOrderStatus enum or raw Maximo code)
get_work_order Fetch a single work order by wonum
create_work_order Create new work orders
update_work_order Update status, assignee, or description — see How updates are sent
read_spare_parts Read inventory items (mxinventory object structure) — filter by sku; an asset_id filter is refused with ConnectorUnsupportedFilterError (see Known Limitations)
read_maintenance_plans Read PM triggers (mxpm object structure)
read_maintenance_history Read an asset's completed and closed work orders (mxwo, status COMP or CLOSE)

Convenience methods

These methods are available but are not declared as agent-discoverable capabilities:

Method Description
close_work_order(wonum) Transition to CLOSED (Maximo CLOSE) via update_work_order
cancel_work_order(wonum) Transition to CANCELLED (Maximo CAN) via update_work_order

Usage Examples

Read assets

assets = await connector.read_assets()
for asset in assets:
    print(f"{asset.id}: {asset.name} (criticality: {asset.criticality})")

Filter work orders with Machina enum

from machina.domain.work_order import WorkOrderStatus

wos = await connector.read_work_orders(
    asset_id="PUMP-201",
    status=WorkOrderStatus.IN_PROGRESS,  # auto-mapped to Maximo "INPRG"
)

Get a single work order

wo = await connector.get_work_order("WO-001")
if wo:
    print(f"{wo.id}: {wo.failure_mode} — {wo.failure_cause}")

Create a work order

from datetime import datetime, timezone
from machina.domain import WorkOrder, WorkOrderType, Priority

wo = WorkOrder(
    id="",
    type=WorkOrderType.CORRECTIVE,
    priority=Priority.HIGH,
    asset_id="PUMP-201",
    description="Replace mechanical seal",
    created_at=datetime.now(tz=timezone.utc),
    updated_at=datetime.now(tz=timezone.utc),
)
created = await connector.create_work_order(wo)
print(f"Created: {created.id}")

Update / close a work order

from machina.domain.work_order import WorkOrderStatus

updated = await connector.update_work_order(
    "WO-001",
    status=WorkOrderStatus.COMPLETED,
    assigned_to="john.doe",
)
await connector.close_work_order("WO-001")

How reads select attributes

Maximo answers a collection query with each member's href and nothing else unless the query names the attributes it wants in oslc.select. Each read therefore selects the attributes its mapping uses, plus siteid, which together with the record's number forms its key:

Read Object structure oslc.select
read_assets, get_asset mxasset assetnum,siteid,description,location,manufacturer,serialnum,priority,parent,classstructureid,assettype
read_work_orders, get_work_order, read_maintenance_history mxwo wonum,siteid,description,status,wopriority,worktype,assetnum,lead,assignedownergroup,failurecode,problemcode,reportdate,changedate
read_spare_parts mxinventory itemnum,siteid,location,minlevel,item.description,invbalances{curbal},invcost{avgcost,lastcost}
read_maintenance_plans mxpm pmnum,siteid,assetnum,description,frequency,frequnit,status
  • Only documented attributes are named. Each name is documented on the object structure's main object, a child object (invbalances{…}) or a related object (item.description), because Maximo may refuse a query that names an attribute the object does not have. ASSET has no model number, and the failure remark is a separate object, so Asset.model stays empty and WorkOrder.failure_cause comes from problemcode.
  • Null attributes are left out. Maximo omits null attributes from its answer, so a missing attribute reads as the field's default: an empty string, no parent, zero stock, criticality C. A PM with no frequency unit gets no calendar interval rather than its frequency read as days. Only the record's number is required: a record that carries nothing else is read, with every other field at its default.
  • Incomplete records end the read. This covers a record without its number (assetnum, wonum, itemnum or pmnum), one whose number is not text or a whole number, and a record the mapping cannot read. A member that carries only its href, as on a page that lost the selection, is such a record. The read raises ConnectorSchemaError (a ConnectorError); no partial list is returned. The message names the object structure and the identity attribute, or the error type and, for a value the domain model rejects, the fields rejected: Maximo returned a mxinventory record that cannot be mapped (ValidationError: stock_quantity) — refusing to return a partial result. A maximo_member_refused warning adds the read, the record's position in it and, where it applies, the asset the read is for. Neither the error nor the warning repeats a value from Maximo or a URL from its answer.
  • Negative balances and costs end the read. A Maximo organization can allow negative current balances, but SparePart holds no negative stock, cost or reorder point, and the connector does not report a figure Maximo does not hold. A spare part whose bin balances add up to less than zero, or whose cost or minlevel is negative, is therefore refused like any record the mapping cannot read (ValidationError: stock_quantity, unit_cost or reorder_point). A read filtered by sku, through the Python API (read_spare_parts(sku=...)) or the MCP tool machina_list_spare_parts (its optional sku parameter), still returns any other item; an unfiltered read fails for as long as any item has a negative balance. Bin balances are added as decimals, and the total is rounded down to whole units, so a total of −0.5 is refused too.
  • Next pages use Maximo's link. Each next page is requested at the link Maximo returns, which repeats the query; the selection is not added again.
  • metadata holds the rest. It keeps the selected attributes that no field takes, such as siteid, and the record's rest id as rest_id: the last segment of its URI, which maximo_raw_attribute_update takes as resource_id. It leaves out OSLC's own keys (href, localref, _imagelibref and *_collectionref name the host Maximo sees itself on, and _rowstamp is a locking token) and the nested objects or lists no field takes, which in a full resource read are links. rest_id comes from Maximo's answer and is treated as untrusted. It is kept, decoded, only for a record's URI (…/os/<object structure>/<rest id>) and only if, percent-encoded as one path segment, it cannot leave that segment. maximo_raw_attribute_update checks it again, like any resource_id, encodes it and sends the write to the configured url, never to the host in href. Code that builds a URL from it must percent-encode it the same way (urllib.parse.quote(rest_id, safe="")).
  • Spare-part reads cost Maximo the most. IBM's Performance Tips note that a related attribute in dot notation, such as item.description, adds a database query for each record returned. On a large inventory, pass sku where you can.

The selections follow IBM's REST API guide (Selecting) and Maximo's data dictionary. They have not been checked against a live Maximo.

How updates are sent

Maximo does not address a record by its wonum. Each record has a URI whose last segment is a rest id that the server derives from the record's primary key (wonum and siteid for a work order), and IBM documents an update as a POST to that URI with an x-method-override: PATCH header. So update_work_order, and close_work_order / cancel_work_order, which go through it:

  1. look the order up by wonum (GET /maximo/oslc/os/mxwo?oslc.where=wonum="WO-001"&oslc.select=wonum,siteid&oslc.pageSize=2&lean=1), following the answer's next pages until it holds two rows;
  2. refuse, before writing anything, a wonum that matches no work order, matches work orders in more than one site (a wonum is unique only within a site), or whose one match does not name that wonum (compared without regard to case), as a server or proxy that dropped the filter would answer;
  3. send POST /maximo/oslc/os/mxwo/{rest id} with x-method-override: PATCH and patchtype: MERGE (and lean=1, unless the connector was created with lean=False), and accept 204 No Content or 200 OK. Without patchtype: MERGE, Maximo deletes the child objects of a listed child collection that the request does not repeat;
  4. read the order back from the same URI (GET /maximo/oslc/os/mxwo/{rest id}, with lean=1 like the update) and return it.

Only the rest id is taken from the URI Maximo returns. Its host is the one Maximo sees itself on (an internal name behind a proxy), so the update and the read-back go to the configured url, like every other request. The rest id is sent as one percent-encoded path segment; a URI that is not a work-order URI, a rest id with an escape that is not UTF-8, or one that is empty or has an empty, . or .. part between / or \ separators once decoded, is refused with ConnectorError before anything is written. No error repeats the URI. Each refusal after the lookup is logged at WARNING as maximo_update_refused, with the wonum and a reason (not_found, several_sites, other_work_order, no_work_order_uri or invalid_rest_id).

The update POST is retried on 429 only. After a network error, a timeout or a 503 it is not sent again, because the first request may have been applied. The lookup and the read-back are reads and are retried like any other. If Maximo accepted the update but the order cannot be read back, ConnectorError says that the update was applied and must not be repeated, and work_order_update_not_read_back is logged at WARNING with the error type.

The maximo_raw_attribute_update MCP vendor tool writes through the same helper, so it sends the same POST to the rest id it is given.

Sources: the IBM Maximo REST API guide, Create and update and Selecting, and update() / merge() in IBM's reference Java client, MaximoConnector.java.

Asset Type Mapping

Maximo does not expose a direct equipment-type field. By default, all assets are classified as ROTATING_EQUIPMENT. Provide an asset_type_map to override based on your Maximo classstructureid (or assettype) values:

from machina.domain.asset import AssetType

connector = Maximo(
    url="https://maximo.example.com",
    auth=auth,
    asset_type_map={
        "PUMPS": AssetType.ROTATING_EQUIPMENT,
        "VESSELS": AssetType.STATIC_EQUIPMENT,
        "INSTRUMENTS": AssetType.INSTRUMENT,
        "MOTORS": AssetType.ELECTRICAL,
    },
)

Unmapped values fall back to ROTATING_EQUIPMENT.

Entity Mapping

Maximo Field Machina Field
assetnum Asset.id
description Asset.name
location Asset.location
manufacturer Asset.manufacturer
serialnum Asset.serial_number
priority (1-3) Asset.criticality (A/B/C; no priority is C)
parent Asset.parent
classstructureid (else assettype) Asset.type (via asset_type_map)
wonum WorkOrder.id
worktype WorkOrder.type (CM→Corrective, PM→Preventive, CP→Predictive, EV→Improvement)
wopriority WorkOrder.priority (1→Emergency, 2→High, 3→Medium, 4→Low)
status WorkOrder.status (WAPPR→Created, APPR→Assigned, INPRG→InProgress, COMP→Completed, CLOSE→Closed, CAN→Cancelled)
assetnum WorkOrder.asset_id
lead (else assignedownergroup) WorkOrder.assigned_to
failurecode WorkOrder.failure_mode
problemcode WorkOrder.failure_cause
reportdate / changedate WorkOrder.created_at / WorkOrder.updated_at
itemnum SparePart.sku
item.description SparePart.name
sum of invbalances.curbal (one row per bin or lot) SparePart.stock_quantity (added as decimals, rounded down to whole units; a total below zero is refused)
minlevel SparePart.reorder_point (rounded down; a negative value is refused)
invcost.avgcost (else lastcost), first row SparePart.unit_cost (a negative cost is refused)
location (the storeroom) SparePart.warehouse_location
pmnum MaintenancePlan.id
assetnum MaintenancePlan.asset_id
frequency × frequnit MaintenancePlan.interval (DAYS, WEEKS, MONTHS; YEARS as twelve months; no unit or another unit gives no interval)
status (ACTIVE) MaintenancePlan.active
siteid metadata["siteid"] (assets, work orders, spare parts)
last segment of href metadata["rest_id"] (assets, work orders, spare parts)

Selected attributes that no field takes stay in metadata. A spare part's reorder flag (yes/no, not a quantity) is one of them when Maximo sends it.

Resilience

All HTTP calls route through a shared retry helper with exponential backoff. See SAP PM Connector — Resilience for details.

Known Limitations

  • Object structure customisation: The connector targets standard Maximo object structures (mxasset, mxwo, mxinventory, mxpm). Custom object structures require subclassing.
  • Spare parts by asset: read_spare_parts(asset_id=...) is refused with ConnectorUnsupportedFilterError (a ConnectorError) before any request, with or without a sku, because the connector reads spare parts from mxinventory, which it does not query by asset. Look parts up by sku instead. Asset-scoped spare parts for Maximo are not supported yet.
  • Filter values: asset and work-order numbers, SKUs and statuses are matched exactly through oslc.where. Maximo documents no escape inside its string literals, and its QBE framework reads wildcards (%, *, ?), value lists (,, [ ]) and operators (=, !, <, >, ~) inside them as query syntax. A value that contains any of " \ % * ? , [ ] = ! < > ~ or a non-printable character, is blank, or is the keyword null is therefore refused with ConnectorError before any request, including the lookup update_work_order sends before writing. The message names the offending characters, not the value. A number is matched as its digits.
  • Work-order numbers never reach a URL path: update_work_order matches the wonum in oslc.where and sends the update to the rest id Maximo returns (see How updates are sent). That rest id is sent as one percent-encoded path segment (/ becomes %2F), and one with an empty, . or .. part between / or \ separators is refused with ConnectorError, so a proxy that decodes %2F cannot resolve it into another resource.
  • The same wonum in several sites: there is no site setting yet. An update refuses such a wonum; get_work_order returns the first match.
  • Not validated against a live Maximo: the attribute selections (see How reads select attributes) and the response shapes the tests use follow IBM's REST API guide and Maximo's data dictionary. In particular:
  • Selections per object structure. A site's object structure can differ from the standard one (a customised MXASSET, say). IBM does not document what Maximo does with a selected attribute that the object structure lacks. If it refuses the query, the read fails with ConnectorError and the HTTP status; if it leaves the attribute out instead, the field reads as its default.
  • Inventory child objects. read_spare_parts expects MXINVENTORY to expose the balances and costs as the child objects invbalances and invcost, and the item as the related object item. If it does not and Maximo leaves them out of its answer rather than refusing the query, every spare part reads with no stock, no cost and no name.
  • Next-page links. Next pages are requested at the nextPage link Maximo returns, which IBM documents as repeating the query. Whether that link keeps oslc.select has not been checked: a page that lost it ends the read with ConnectorSchemaError, not a shorter list.
  • Custom frequency units: a PM with no frequnit, or one that is not DAYS, WEEKS, MONTHS or YEARS (a site-defined synonym, say), gets an empty interval, so it is reported as having no calendar due date. A month counts as 30 days in Interval.total_days, so a yearly PM's interval is 360 days.
  • Status changes: a status update is sent as the plain status attribute. IBM's REST API guide shows status changes only through actions that call changeStatus and does not say whether mxwo applies the attribute directly; this has not been checked against a live Maximo. A rejected update raises ConnectorError with the HTTP status.
  • Pagination: Follows the OSLC responseInfo.nextPage.href link of each page (a bare URL or a relative link is followed too). Each page is requested with the connector's credentials, so a link to another scheme, host or port than the configured url, or one with user info in it, raises ConnectorError and logs maximo_next_page_refused with the link's scheme and host: Maximo builds its links from its own configured address, so behind a proxy check that it returns the address Machina uses. A nextPage with no usable URL, a link the server already returned, more than 50,000 rows in one read, or more pages than those rows fill also raise ConnectorError instead of ending the read early or growing without bound. get_asset and get_work_order read only the first page: they use the first match. Very large result sets may benefit from server-side oslc.where filtering.

API Reference

MaximoConnector

MaximoConnector(*, url: str, auth: _AuthUnion | dict[str, Any], lean: bool = True, asset_type_map: dict[str, AssetType] | None = None)

Connector for IBM Maximo Manage.

Provides integration with Maximo's OSLC/JSON REST API for reading and creating maintenance data.

Parameters:

Name Type Description Default
url str

Base URL of the Maximo instance (e.g. https://maximo.example.com).

required
auth _AuthUnion | dict[str, Any]

Authentication strategy — :class:ApiKeyHeaderAuth, :class:BasicAuth, or :class:BearerAuth, or the equivalent dict selected by type (api_key, basic, bearer) as a machina.yaml settings block carries it.

required
lean bool

If True (default), requests add lean=1 to suppress OSLC namespace wrappers in responses.

True
asset_type_map dict[str, AssetType] | None

Optional mapping from a Maximo classification key (classstructureid preferred, assettype as fallback) to a Machina :class:AssetType. When omitted, every Maximo asset is mapped to :attr:AssetType.ROTATING_EQUIPMENT. Use this to honour your site's Maximo taxonomy without subclassing the connector.

None
Example
from machina.connectors import Maximo
from machina.connectors.cmms import ApiKeyHeaderAuth
from machina.domain.asset import AssetType

connector = Maximo(
    url="https://maximo.example.com",
    auth=ApiKeyHeaderAuth(header_name="apikey", value="my-key"),
    asset_type_map={
        "PUMPS": AssetType.ROTATING_EQUIPMENT,
        "VESSELS": AssetType.STATIC_EQUIPMENT,
        "INSTRUMENTS": AssetType.INSTRUMENT,
    },
)
await connector.connect()
assets = await connector.read_assets()

connect async

connect() -> None

Verify credentials against the Maximo API.

Performs a lightweight request to the whoami endpoint.

Raises:

Type Description
ConnectorAuthError

If credentials are invalid.

ConnectorError

If the server is unreachable.

disconnect async

disconnect() -> None

Close the connector.

health_check async

health_check() -> ConnectorHealth

Return current health status.

read_assets async

read_assets() -> list[Asset]

Return all assets from Maximo (MXASSET object structure).

Raises:

Type Description
ConnectorSchemaError

If a record lacks its assetnum or cannot be mapped; no partial list is returned.

ConnectorError

On an HTTP or paging error (see :meth:_oslc_get).

get_asset async

get_asset(asset_id: str) -> Asset | None

Look up a single asset by asset number (the first match).

Raises:

Type Description
ConnectorSchemaError

If the record lacks its assetnum or cannot be mapped.

ConnectorError

If asset_id is refused by :func:_oslc_eq, or on an HTTP error.

read_work_orders async

read_work_orders(*, asset_id: str = '', status: WorkOrderStatus | str = '') -> list[WorkOrder]

Read work orders, optionally filtered by asset or status.

Parameters:

Name Type Description Default
asset_id str

Filter by Maximo asset number.

''
status WorkOrderStatus | str

Filter by status — accepts a :class:WorkOrderStatus enum (reverse-mapped to Maximo code) or a raw Maximo status string for backward compatibility.

''

Raises:

Type Description
ConnectorSchemaError

If a record lacks its wonum or cannot be mapped; no partial list is returned.

ConnectorError

If a filter value is refused by :func:_oslc_eq, or on an HTTP or paging error (see :meth:_oslc_get).

get_work_order async

get_work_order(work_order_id: str) -> WorkOrder | None

Look up a single work order by work order number (the first match).

Raises:

Type Description
ConnectorSchemaError

If the record lacks its wonum or cannot be mapped.

ConnectorError

If work_order_id is refused by :func:_oslc_eq, or on an HTTP error.

create_work_order async

create_work_order(work_order: WorkOrder) -> WorkOrder

Create a new work order in Maximo.

Parameters:

Name Type Description Default
work_order WorkOrder

Machina :class:WorkOrder to create.

required

Returns:

Type Description
WorkOrder

The created work order with the server-assigned wonum.

update_work_order async

update_work_order(work_order_id: str, *, status: WorkOrderStatus | None = None, assigned_to: str | None = None, description: str | None = None) -> WorkOrder

Update an existing work order in Maximo.

Maximo addresses a record by the URI it returns for it, not by wonum, so the order is first looked up by wonum (:meth:_work_order_rest_id), the update is sent to its URI through :meth:_patch_resource, and the order is read back from the same URI. Only non-None fields are sent. close_work_order and cancel_work_order go through this method.

Parameters:

Name Type Description Default
work_order_id str

Maximo work order number (wonum).

required
status WorkOrderStatus | None

New status (reverse-mapped to Maximo code).

None
assigned_to str | None

New lead person.

None
description str | None

New work order description.

None

Returns:

Type Description
WorkOrder

The updated work order, read back from its URI.

Raises:

Type Description
ConnectorError

If no field is given; if the lookup refuses the wonum (see :meth:_work_order_rest_id), before anything is written; if Maximo rejects the update; or if Maximo accepted the update but the order could not be read back, in which case the message says the update was applied and must not be repeated.

ConnectorAuthError

If Maximo answers the lookup or the update with 401.

HTTPError

A transport error from the lookup (after its retries) or from the update, which is not retried.

close_work_order async

close_work_order(work_order_id: str) -> WorkOrder

Transition a work order to CLOSED status.

cancel_work_order async

cancel_work_order(work_order_id: str) -> WorkOrder

Transition a work order to CANCELLED status.

read_spare_parts async

read_spare_parts(*, asset_id: str = '', sku: str = '') -> list[SparePart]

Read inventory items (spare parts) from Maximo.

Parameters:

Name Type Description Default
asset_id str

Not supported yet: the connector reads spare parts from mxinventory, which it does not query by asset. An empty string means no filter; any other value is refused, with or without sku, rather than dropped.

''
sku str

Optional Maximo itemnum to narrow the lookup via an OSLC where clause. An empty string means no filter.

''

Raises:

Type Description
ConnectorUnsupportedFilterError

If asset_id is given, before any request.

ConnectorSchemaError

If a record lacks its itemnum or cannot be mapped; no partial list is returned.

ConnectorError

If a filter is neither "" nor an ID (see :func:~machina.connectors.cmms.filters.spare_part_filters), if sku is refused by :func:_oslc_eq, or on an HTTP or paging error (see :meth:_oslc_get).

read_maintenance_plans async

read_maintenance_plans() -> list[MaintenancePlan]

Read preventive-maintenance plans (PMs) from Maximo.

Raises:

Type Description
ConnectorSchemaError

If a record lacks its pmnum or cannot be mapped; no partial list is returned.

ConnectorError

On an HTTP or paging error (see :meth:_oslc_get).

read_maintenance_history async

read_maintenance_history(asset_id: str) -> list[WorkOrder]

Return completed/closed work orders for an asset.

Raises:

Type Description
ConnectorSchemaError

If a record lacks its wonum or cannot be mapped; no partial list is returned.

ConnectorError

If asset_id is missing or blank (a missing ID would otherwise be matched as the text "None") or refused by :func:_oslc_eq, or on an HTTP or paging error (see :meth:_oslc_get).