Skip to content

UpKeep Connector

The UpKeepConnector integrates Machina with UpKeep, a leading cloud-based CMMS platform, via its REST API v2.

Prerequisites

  • UpKeep account with API access enabled
  • API token (generated from Account Settings → API Tokens in the UpKeep web UI)

Installation

pip install machina-ai[cmms-rest]

Configuration

from machina.connectors import UpKeep

connector = UpKeep(api_key="your-upkeep-api-token")
await connector.connect()
connectors:
  cmms:
    type: upkeep
    primary: true
    settings:
      api_key: ${UPKEEP_API_KEY}

Capabilities

Capability Description
read_assets Read all assets (/api/v2/assets)
read_work_orders Read work orders — filter by asset_id and/or status (accepts WorkOrderStatus enum or raw UpKeep string)
get_work_order Fetch a single work order by ID
create_work_order Create a new work order
update_work_order Update status, assignee, or description via PATCH
read_spare_parts Read the parts inventory (/api/v2/parts), or with asset_id only the parts that asset lists (the asset, then each listed part); sku narrows either in memory — prefers partNumber / barcode as SKU
read_maintenance_plans Read PM templates with their schedules (/api/v2/pm) — one plan per PM schedule
read_maintenance_history Read an asset's completed work orders (/api/v2/work-orders, status complete)

Convenience methods

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

Method Description
close_work_order(id) Transition to CLOSED (maps to UpKeep complete) via update_work_order
cancel_work_order(id) Transition to CANCELLED (maps to UpKeep onHold) via update_work_order

Usage Examples

Read assets

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

Read work orders with Machina enum filter

from machina.domain.work_order import WorkOrderStatus

wos = await connector.read_work_orders(
    asset_id="asset-123",
    status=WorkOrderStatus.IN_PROGRESS,  # auto-mapped to "inProgress"
)

Get a single work order

wo = await connector.get_work_order("wo-123")

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="asset-123",
    description="Replace worn bearing",
    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-123",
    status=WorkOrderStatus.COMPLETED,
    assigned_to="tech-user-id",
)
await connector.close_work_order("wo-123")

Entity Mapping

UpKeep Field Machina Field
id Asset.id
name Asset.name
category Asset.type (mapped to closest AssetType)
location Asset.location
make Asset.manufacturer
model Asset.model
serialNumber Asset.serial_number
id (work order) WorkOrder.id
title WorkOrder.description
priority (0-3) WorkOrder.priority (0→Low, 1→Medium, 2→High, 3→Emergency)
status WorkOrder.status (open→Created, inProgress→InProgress, onHold→Assigned, complete→Completed; filters and updates send the same values, with Closed as complete and Cancelled as onHold)
partNumber / barcode / id SparePart.sku (prefers physical identifier, falls back to record ID)
name (part) SparePart.name
quantity SparePart.stock_quantity
id / _id (PM schedule) MaintenancePlan.id
asset (PM schedule) MaintenancePlan.asset_id
repeatFrequency × repeatInterval (PM schedule) MaintenancePlan.interval (DAILY→days, WEEKLY→weeks, MONTHLY→months, YEARLY→12 months)
endDate / scheduleHasEnded (PM schedule) MaintenancePlan.active (false once the end date has passed or UpKeep marks the schedule ended)
name (PM template) MaintenancePlan.name
tasks[].name (PM template) MaintenancePlan.tasks
estimatedTime (PM template, hours) MaintenancePlan.estimated_duration_hours

Resilience

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

Known Limitations

  • Asset criticality: UpKeep does not expose a native criticality field. All assets default to Criticality.C.
  • Work order types: UpKeep uses category ("preventive" / "reactive"). The connector maps these to PREVENTIVE and CORRECTIVE respectively. Predictive and improvement types are not natively supported by UpKeep; for custom categories, subclass the connector.
  • Spare parts by asset: /api/v2/parts has no asset filter, so read_spare_parts(asset_id=...) reads the asset (GET /api/v2/assets/<ID>), whose parts list holds the IDs of its parts, then fetches each listed part (GET /api/v2/parts/<ID>). That is 1 + k requests for k listed parts; the inventory is not paged. A part listed twice is fetched and returned once. An asset that lists no parts returns an empty list after the one request.
    • Limit: an asset listing more than 100 parts is refused with ConnectorError before any part request.
    • Agent prefetch budget: the agent's per-turn context prefetch waits at most 5 seconds for an asset's spare parts from UpKeep. A read still running at the deadline is cancelled where it waits. Its pending request and its HTTP client end with it, and no further part is requested. The prompt then says the parts were not checked because the lookup did not finish in time, and names the check_spare_parts tool, which runs the full read. The budget is a policy for the eager per-turn lookup, not a completeness guarantee. Within it the read is the same all-or-none read. The check_spare_parts tool, the MCP machina_list_spare_parts tool, workflows and direct read_spare_parts calls have no such deadline.
    • Refusals: the read returns all of the asset's parts or none. It raises ConnectorError when UpKeep has no such asset, or cannot return a part the asset lists (HTTP 404). It raises ConnectorSchemaError when UpKeep's answer is not a well-formed record of that asset (a record with another id, a body without one, or a malformed parts list), when a part record is malformed or has a field the mapping cannot read (a quantity or cost that is not a number, a negative stock or cost), or when a part request returns a different part. Only the asset's own record can say it lists no parts. A connection error or a timeout that outlasts the retries propagates as the httpx error, as on the connector's other reads; the parts already read are not returned. No refusal message repeats a value from UpKeep, and Machina's retry warnings name a retried part request as /api/v2/parts/<listed part>, without the part's ID. (configure_logging() also keeps httpx's own per-request lines, which carry full URLs, out of the log.)
    • SKU: a sku narrows the asset's parts in memory, so both filters hold. Without asset_id, the connector pages /api/v2/parts and filters by sku in memory; a part record the mapping cannot read refuses that read too, with ConnectorSchemaError.
    • Not validated live: this follows UpKeep's documented API reference and has not been validated against a live UpKeep instance.
  • Record IDs in paths: an asset, work-order or part ID is sent as one percent-encoded path segment (/ becomes %2F). An empty ID, or one with an empty, . or .. part between / or \ separators, is refused before any request: with ConnectorError for an ID you pass, and with ConnectorSchemaError, refusing the whole read, for a part ID in an asset's parts list.
  • Failure data: UpKeep has no standard failure-mode fields. Failure-related data may be available in WorkOrder.metadata depending on your UpKeep configuration.
  • Maintenance plans: a PM template can schedule several assets, each with its own recurrence, so the connector returns one plan per schedule (soft-deleted templates are skipped). A meter-triggered schedule maps with an empty interval: the PM payload names the meter but not its unit. Legacy PM triggers (/api/v2/preventive-maintenance) are not read — their documented responses carry no asset, and PM templates reference them through legacyPMId.

API Reference

UpKeepConnector

UpKeepConnector(*, url: str = '', api_key: str = '')

Connector for UpKeep CMMS.

Provides integration with UpKeep's REST API v2 for reading and creating maintenance data.

Parameters:

Name Type Description Default
url str

UpKeep API base URL. Defaults to the production endpoint.

''
api_key str

API token from UpKeep (passed as Session-Token header).

''
Example
from machina.connectors import UpKeep

connector = UpKeep(api_key="your-upkeep-api-token")
await connector.connect()
assets = await connector.read_assets()

connect async

connect() -> None

Verify credentials against the UpKeep API.

Raises:

Type Description
ConnectorAuthError

If the API key is missing or invalid.

ConnectorError

If the API is unreachable.

disconnect async

disconnect() -> None

Close the connector (no persistent connections to clean up).

health_check async

health_check() -> ConnectorHealth

Return current health status.

read_assets async

read_assets() -> list[Asset]

Return all assets from UpKeep.

get_asset async

get_asset(asset_id: str) -> Asset | None

Look up a single asset by ID.

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 UpKeep asset ID.

''
status WorkOrderStatus | str

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

''

get_work_order async

get_work_order(work_order_id: str) -> WorkOrder | None

Look up a single work order by ID.

create_work_order async

create_work_order(work_order: WorkOrder) -> WorkOrder

Create a new work order in UpKeep.

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 ID.

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 UpKeep via PATCH.

Only non-None fields are included in the PATCH payload.

Parameters:

Name Type Description Default
work_order_id str

UpKeep work order ID.

required
status WorkOrderStatus | None

New status (reverse-mapped to UpKeep string).

None
assigned_to str | None

New assignedToId.

None
description str | None

New title.

None

Returns:

Type Description
WorkOrder

The updated work order.

close_work_order async

close_work_order(work_order_id: str) -> WorkOrder

Transition a work order to CLOSED (maps to 'complete' in UpKeep).

cancel_work_order async

cancel_work_order(work_order_id: str) -> WorkOrder

Transition a work order to CANCELLED (maps to 'onHold' in UpKeep).

read_spare_parts async

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

Read spare parts (UpKeep calls them parts).

Without asset_id the whole parts inventory is paged from /api/v2/parts. With it, the asset is read and each part its parts list names is fetched (see Note).

Parameters:

Name Type Description Default
asset_id str

Optional UpKeep asset ID: returns only the parts that asset lists. Empty means no asset filter.

''
sku str

Optional SKU / part number to filter the result in-memory after fetching. Matches the parsed :attr:SparePart.sku, which prefers the physical part identifier. With asset_id, both filters must hold.

''

Raises:

Type Description
ConnectorError

If a filter is neither "" nor an ID (see :func:~machina.connectors.cmms.filters.spare_part_filters), the asset ID cannot be one URL path segment, UpKeep has no such asset, the asset lists more than 100 parts, UpKeep cannot return a part the asset lists (HTTP 404), or a request answers another error status.

ConnectorAuthError

If UpKeep rejects the API key.

ConnectorSchemaError

If UpKeep does not return a well-formed record of the asset (another record, or a malformed parts list), a part record is malformed or has fields that cannot be mapped, or a part request returns another part. No partial list is returned.

TransportError

If a request still fails after its retries (a connection error or a timeout); no partial list is returned.

Note

UpKeep's /api/v2/parts endpoint has no asset filter; the asset record (GET /api/v2/assets/<ID>) lists the IDs of its parts, and each is fetched with GET /api/v2/parts/<ID>: 1 + k requests for k listed parts, never the whole inventory. A part listed twice is fetched and returned once. Any part that cannot be read refuses the whole read rather than returning the others as the asset's complete list.

read_maintenance_plans async

read_maintenance_plans() -> list[MaintenancePlan]

Read preventive-maintenance plans from UpKeep's PM templates.

Returns one plan per PM schedule: the asset and the recurrence live on each schedule, the name and tasks on its template. Legacy PM triggers (/api/v2/preventive-maintenance) are not read — their documented responses carry no asset.

read_maintenance_history async

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

Return completed work orders for an asset.

Raises:

Type Description
ConnectorError

If asset_id is missing or blank. read_work_orders reads an empty asset_id as "no filter", so the read would return every completed work order in the account as this asset's history.