Mapping Work Order Types to Utility Network Asset Types

The integration between a maintenance system and a network model usually exchanges identifiers and status, and leaves the semantics to convention. A work order says “replace” and somebody, later, remembers to retire the old asset and commission the new one — or does not, and the model keeps a main that was cut out of the ground last spring. The fix is a mapping written as data: for each work-order type, which asset types it may target, which lifecycle transition it implies, and what model change closing it should produce. That mapping turns an exchange of identifiers into a contract both systems can validate against, before dispatch and again at closure. It extends the identity and ownership discipline in CMMS and GIS integration for work orders.

Environment Prerequisites

  • A published work-order type list from the maintenance system, stable enough to map against. A free-text type field cannot be mapped and should be constrained first.
  • The asset type taxonomy from the network model, and the crosswalk between the two systems’ identifiers.
  • The lifecycle transition map, so an implied transition can be checked for legality from the asset’s current state.
  • Python 3.11 with requests>=2.31 for the exchange; no spatial libraries are needed because this layer reasons about types and states.
  • A rejection route back into the maintenance system, since a work order that fails validation has to reach the planner who raised it.
  • A versioned mapping file, because a change to it changes what the two systems consider legal.

Schema-Aware Validation Protocol — Run Before Accepting Orders

  1. Confirm every active work-order type appears in the mapping. An unmapped type is accepted by default in most integrations, which is how a replacement order comes to have no model effect.
  2. Check the asset-type lists against the deployed taxonomy. A mapping written against an older asset package silently rejects valid orders after a schema upgrade.
  3. Verify the implied transitions are legal in the state machine. A mapping that implies a transition the machine forbids will fail at closure, after the work is done.
  4. Test the rejection path. A rejection that does not reach a planner is a work order that quietly stops existing.
  5. Reconcile the crosswalk coverage. Orders referencing assets with no crosswalk entry cannot be validated at all, and their count is the size of the identity gap.

Minimal Reproducible Implementation

from __future__ import annotations

import logging
from dataclasses import dataclass, field

logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
LOG = logging.getLogger("work-order-map")

# work-order type -> what the network model permits and expects
MAPPING = {
    "INSPECTION": {"asset_types": {"*"}, "transition": None, "edit": "NONE"},
    "REPAIR": {"asset_types": {"*"}, "transition": None, "edit": "ATTRIBUTES"},
    "REPLACEMENT": {"asset_types": {"*"}, "transition": ("ACTIVE", "RETIRED"),
                     "edit": "GEOMETRY_AND_ASSOCIATIONS"},
    "RETIREMENT": {"asset_types": {"*"}, "transition": ("ACTIVE", "ABANDONED"),
                    "edit": "ASSOCIATIONS"},
    "NEW_CONNECTION": {"asset_types": {"ServicePoint", "ServiceLateral"},
                        "transition": ("PROPOSED", "ACTIVE"),
                        "edit": "GEOMETRY_AND_ASSOCIATIONS"},
}

LEGAL_TRANSITIONS = {
    "PROPOSED": {"ACTIVE", "CANCELLED"},
    "ACTIVE": {"ABANDONED", "RETIRED"},
    "ABANDONED": {"RETIRED", "ACTIVE"},
    "RETIRED": set(),
}


@dataclass
class Verdict:
    order_id: str
    accepted: bool
    reason: str = ""
    required_edit: str = "NONE"


@dataclass
class IntakeReport:
    verdicts: list[Verdict] = field(default_factory=list)

    @property
    def rejected(self) -> list[Verdict]:
        return [v for v in self.verdicts if not v.accepted]


def validate_order(order_id: str, order_type: str, asset_type: str,
                   current_state: str) -> Verdict:
    """Check an incoming work order against the mapping and the state machine.

    Two checks, both cheap and both before dispatch: may this order type target this
    asset type, and is the transition it implies legal from where the asset is now.
    """
    spec = MAPPING.get(order_type)
    if spec is None:
        return Verdict(order_id, False,
                       f"work-order type {order_type} is not mapped")

    allowed = spec["asset_types"]
    if "*" not in allowed and asset_type not in allowed:
        return Verdict(order_id, False,
                       f"{order_type} may not target asset type {asset_type}")

    transition = spec["transition"]
    if transition is not None:
        expected_from, expected_to = transition
        if current_state != expected_from:
            return Verdict(order_id, False,
                           f"{order_type} expects an asset in {expected_from}, "
                           f"found {current_state}")
        if expected_to not in LEGAL_TRANSITIONS.get(current_state, set()):
            return Verdict(order_id, False,
                           f"{current_state} to {expected_to} is not a legal transition")

    return Verdict(order_id, True, required_edit=spec["edit"])


def on_close(order_id: str, order_type: str) -> str:
    """The model change a closed order of this type should produce."""
    spec = MAPPING.get(order_type, {})
    effect = spec.get("edit", "NONE")
    LOG.info("order %s closed: model effect %s", order_id, effect)
    return effect
A worked mapping from work-order type to what the network model must permit Every work-order type implies something about the asset it targets: which asset types it can legitimately apply to, which lifecycle transition it is allowed to drive, and whether it requires a network edit at all. Writing that down turns the integration from a free-text exchange into a contract two systems can both check. Work-order type Valid asset types Lifecycle effect Network edit Inspection any in service none none Repair any in service none attributes only Replacement same class as target retire + create geometry + associations Retirement any in service active → abandoned/retired associations New connection service point classes proposed → active geometry + associations Three columns that turn an integration into something both systems can validate.

The wildcard in asset_types is deliberate and worth being careful with. Most order types legitimately apply to anything in service, and enumerating every asset type produces a mapping that breaks on the next schema addition. The types that genuinely need a restricted list — new connections, for instance — are the exception, and they are the ones where an incorrectly targeted order does real damage.

Validating an incoming work order against the network model A work order arriving from the maintenance system is checked twice: does it target an asset type this order type may apply to, and is the lifecycle transition it implies legal from the asset’s current state? A mismatch on the first is usually a mis-typed order; a mismatch on the second is usually a state the network does not agree with, and both are worth catching before a crew is dispatched. A work order arrives referencing an asset type check May this order type target this asset type? no No — reject to the maintenance system with the reason yes Yes — continue to the lifecycle check lifecycle check Is the implied transition legal now? no No — the two systems disagree about the asset state yes Yes — accept and schedule Two cheap checks, both before dispatch rather than after.

Closing the Loop

Validation before dispatch prevents bad orders. The other half is that closing a good one should change the model, and that is where most integrations stop.

An inspection closes with a condition grade and a date, which flow into the risk model. A repair closes with attribute updates. A replacement closes with two lifecycle transitions and an association transfer — retire the old asset, commission the new one, move the containment and attachment rows across. A retirement closes by driving the same gated workflow a manual retirement uses, including the confirming trace.

The important property is that none of this depends on somebody remembering. The order type says what happens, the mapping says how, and a failure at the gate produces a review item rather than a silent skip. That last point matters: an estate where model changes are attempted and sometimes refused is in far better shape than one where they are sometimes simply not attempted, because the first has a queue and the second has a slow divergence nobody is measuring.

What closing a work order should do to the network model A closed order is the trigger for the model change its type implies. An inspection writes a condition grade and a date. A replacement retires the old asset and commissions the new one, transferring associations. A retirement drives the lifecycle transition through the same gated workflow any other retirement uses. Nothing about this happens because a human remembered; it happens because the order type says so. CLOSED order completed in the CMMS LOOK UP type → model effect APPLY in a version, gated as usual CONFIRM result back to the work order validation refuses GATE FAILS the order stays closed and the model change becomes a review item, not a silent skip The order type is what makes the model change automatic rather than remembered.

Production Deployment Pattern

  1. Validate at intake, before scheduling. A rejection that arrives after a crew has been dispatched has already cost more than the check.
  2. Reject to the planner, with the reason. “Replacement orders expect an asset in ACTIVE, found ABANDONED” is actionable and also tells the planner something true about the network.
  3. Version the mapping alongside the schema. An asset package upgrade that adds types is a mapping change, and reviewing them together is what keeps the pair consistent.
  4. Apply the closure effect through the normal gates. The model change from a closed order is not privileged; it goes through the same version, validation and confirming trace.
  5. Report unmapped order types weekly. New types appear as the maintenance organisation evolves, and each unmapped one is a class of work the model never learns about.
  6. Track attempted-and-refused closures. A rising count means the two systems are diverging on asset state, which is a reconciliation problem rather than an integration bug.

Conclusion

Writing the work-order-to-asset mapping down as data converts a convention into a contract. The intake check stops mis-typed orders before dispatch, the lifecycle check surfaces disagreements about asset state while they are cheap, and the closure effect makes the model change a consequence of the work rather than something somebody remembers. What remains is a queue of refusals, which is exactly the artefact an estate needs in order to see where its two systems disagree.

For authoritative reference, consult the Python dataclasses library and the requests documentation.