Technician Routing & SLA Enforcement: Assigning the Right Technician to Hit Every Deadline

A validated work order is only half the job. The ingestion pipeline can produce a perfectly structured, SLA-tagged payload and the facility can still miss its deadline — because a plumber was dispatched to an electrical fault, because the nearest qualified technician was never considered, because an SLA clock ran out with no one watching it, or because a job fired to a technician who clocked out four hours ago. Technician routing and SLA enforcement is the domain that consumes the output of the work order ingestion pipeline and turns each WorkOrderPayload into an assignment that a real person can complete before the requested_completion deadline elapses. This page is the reference architecture for that domain: it frames the operational failures, walks the dispatch-to-escalation topology, then drills into each subsystem — skill matching, geographic zoning, SLA timers, shift availability, and compliance logging — linking out to the guides that implement each one.

Routing sits at the boundary between data and labor. Upstream, everything is deterministic: a schema, a queue, a correlation id. Downstream, the system meets the messy reality of certifications that expire, technicians who call in sick, drive times that blow past an SLA window, and regulators who want proof that a critical safety work order was actioned within four hours. The job of this layer is to make labor assignment as auditable and deterministic as the ingestion that fed it.

The Operational Problem: Wrong-Skill Dispatch, Drive-Time Waste, and Silent Breaches

Dispatch fails in five predictable ways, and a routing engine exists to eliminate all five.

Wrong-skill dispatch is the first and most visible failure. A work order tagged as an electrical fault lands with a general handyman because the dispatcher grabbed the first available name. The technician arrives, cannot legally or safely perform the work, and the job bounces — a wasted truck roll, a second dispatch, and an SLA clock that has already burned half its budget. Certifications are not interchangeable: a refrigerant-handling job requires an EPA 608 card, a high-voltage panel requires a licensed electrician, and dispatching without matching trade to requirement guarantees rework.

Drive-time waste is the second. In a multi-site portfolio, the naive assignment — nearest idle technician by list order — routinely sends someone across a metro area while a qualified technician sits two buildings from the asset. Every unnecessary mile is unbillable time, fuel, and SLA budget spent on a highway instead of a repair. At scale, uncontrolled drive time is often the single largest source of MTTR inflation that has nothing to do with the actual work.

SLA breaches with no escalation are the third and most expensive. A WorkOrderPayload carries a requested_completion timestamp and an escalation_tier, but a timestamp in a database enforces nothing on its own. Without an active timer that fires at defined thresholds, a critical job can sit unassigned until it is already late, and no supervisor is paged until a tenant complains. The deadline exists in the data; the enforcement does not exist until something is actively counting down against it.

After-hours misfires are the fourth. An automated dispatcher that ignores shift calendars will happily assign a 2 a.m. job to a technician whose shift ended at 6 p.m., or route a weekend emergency to a team that is entirely off-rotation. The assignment looks valid in the data and is operationally impossible in reality. Availability is not a static attribute of a technician — it is a function of the shift calendar at the exact moment of dispatch.

Unprovable compliance is the fifth. Frameworks such as ISO 55001 asset management and safety-critical maintenance regimes require that an organization prove, after the fact, that a given work order was routed to a qualified person and completed within its SLA. If the routing engine does not emit a tamper-evident record of every assignment, escalation, and state transition, the facility can do everything right and still fail an audit because it cannot demonstrate that it did.

The architecture described here treats dispatch as a scoring-and-enforcement discipline rather than a clerical decision. Every validated payload is scored against the eligible technician pool, assigned to the best-fit candidate, tracked by an active SLA timer that escalates on breach, and recorded in an immutable audit log. Nothing is assigned by intuition, and nothing that happens goes unrecorded.

Dispatch, Routing, and SLA Enforcement Topology

The routing engine operates as a pipeline of its own, downstream of ingestion. A validated payload enters, passes through eligibility filtering and candidate scoring, is committed as an assignment, and is then held under an active SLA timer until the work order closes or breaches. Each stage has a strict contract so a failure in scoring cannot corrupt the SLA timer state, and vice versa.

Technician routing and SLA enforcement pipeline A validated WorkOrderPayload from the ingestion pipeline enters an eligibility filter that applies skill, zone, and shift constraints. Eligible candidates are scored, the best-fit technician is assigned, and the committed assignment is held under an active SLA timer that either closes on completion or escalates through tiers to a supervisor. Every transition writes to an immutable compliance audit log along the bottom. Technician routing and SLA enforcement pipeline validated payload from ingestion Eligibility skill · zone · shift Score rank candidates Assign commit best-fit SLA timer count down closed on time breach escalate to supervisor Immutable compliance audit log · every assignment, timer event, and escalation
End-to-end topology: a validated payload is filtered for eligibility, scored, assigned, and held under an SLA timer that either closes cleanly or escalates — with every transition written to an immutable audit log.

The Eligibility stage is a hard filter: it removes every technician who cannot legally, geographically, or temporally take the job. The Score stage ranks the survivors on a weighted blend of proximity, skill fit, and current load. The Assign stage commits the top candidate and stamps the assignment with the SLA deadline inherited from the payload. The SLA timer stage then holds the work order under an active countdown that escalates through tiers on breach. Beneath all four, the compliance log records every transition so the whole lifecycle is reconstructable after the fact.

The contract that flows through this pipeline is the same WorkOrderPayload produced upstream, whose shape is governed by the work order schema standards. The routing engine reads its fields but never redefines them; the mandatory SLA triple — priority, requested_completion, and escalation_tier — is exactly the information routing needs to make and enforce an assignment:

from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Dict, List, Optional


class Priority(str, Enum):
    CRITICAL = "critical"
    HIGH = "high"
    STANDARD = "standard"
    PLANNED = "planned"


@dataclass
class WorkOrderPayload:
    """Canonical CMMS work order — SLA fields are mandatory site-wide."""
    work_order_id: str
    asset_id: str
    part_skus: List[str]
    required_quantities: Dict[str, int]
    location_id: str
    priority: Priority = Priority.STANDARD
    requested_completion: Optional[datetime] = None
    escalation_tier: int = 0
    status: str = "open"
    created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))

Everything downstream of ingestion reads these fields. priority seeds the scoring weights, requested_completion arms the SLA timer, and escalation_tier is the counter that the escalation ladder increments on each breach. Because they arrive already populated, the routing engine never has to infer urgency — it enforces urgency that was decided at intake.

Skill-Based Dispatch: Matching Trade and Certification to the Job

The first eligibility gate is competence. A work order names an asset and, through its failure code and trade classification, an implied set of required skills — an EPA refrigerant certification for a chiller, a licensed-electrician credential for a switchgear fault, a confined-space qualification for a below-grade sump. Assigning a technician who lacks the required credential is not a soft preference; it is a hard filter that must exclude them before scoring even begins. The mechanics of encoding certifications, expiries, and trade matrices are covered in skill-based dispatch, which walks the full mapping from a work order’s trade requirement to a technician’s certification set.

Skill matching has three failure modes worth designing against explicitly. The first is the expired credential — a technician who held the right card last quarter but whose EPA 608 lapsed; the eligibility check must compare against the credential’s expiry date, not merely its presence. The second is the over-qualified assignment — routing a master electrician to reset a tripped breaker when an apprentice is idle and closer; skill matching should establish a floor, not force the most senior person onto every job. The third is the missing-skill fallback — when no eligible technician holds the required credential, the work order cannot silently vanish; it must route to a review queue or trigger a contractor-dispatch path, exactly as an unresolvable asset does upstream in ingestion.

The eligibility filter itself is a pure predicate over a technician and a payload. It returns a boolean, and only technicians who pass proceed to scoring:

from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Set


@dataclass
class Technician:
    technician_id: str
    zone_id: str
    certifications: Set[str]           # e.g. {"EPA-608", "HVAC", "ELEC-JOURNEY"}
    cert_expiry: Dict[str, datetime]   # certification -> expiry timestamp
    home_lat: float
    home_lon: float
    active_jobs: int = 0


TRADE_REQUIREMENTS = {
    "FC-HVAC-REFRIG": {"EPA-608", "HVAC"},
    "FC-ELEC-HV": {"ELEC-JOURNEY"},
    "FC-PLUMB": {"PLUMB"},
    "FC-GEN": set(),
}


def is_skill_eligible(tech: Technician, failure_code: str,
                      now: datetime) -> bool:
    """True only if the technician holds every required, unexpired credential."""
    required = TRADE_REQUIREMENTS.get(failure_code, set())
    for credential in required:
        if credential not in tech.certifications:
            return False
        expiry = tech.cert_expiry.get(credential)
        if expiry is not None and expiry <= now:
            return False   # credential lapsed
    return True

By making skill eligibility a hard predicate rather than a scoring weight, the engine guarantees that no unqualified technician can ever win a job on the strength of being close or idle. Competence is non-negotiable; everything else is a matter of degree.

Geographic Zone Routing: Spending SLA Budget on Repairs, Not Highways

Once the pool is narrowed to the competent, the next constraint is distance. Every minute a technician spends driving is a minute subtracted from the SLA budget, so a routing engine that ignores geography will breach deadlines it could easily have met. The durable fix is to partition the portfolio into zones and prefer within-zone assignment, falling back to adjacent zones only when the home zone has no eligible, available technician. The full partitioning strategy — how to draw zone boundaries, handle overlaps, and cost cross-zone dispatch — is covered in geographic zone routing.

Zone routing is where the assignment meets the physical world modeled by the asset registry. A work order’s location_id resolves to a site, and that site belongs to a zone; the asset hierarchy design that governs the registry is what makes location_id meaningful as a geographic anchor rather than an opaque string. The scoring function then blends a proximity term with the skill-fit and load terms, so that among equally qualified technicians the closer one wins, but a slightly more distant expert can still beat a nearby generalist when the job demands it.

Proximity should be a smooth cost, not a binary in-zone flag, because a technician just across a zone boundary may be far closer than one at the far edge of the home zone. A simple great-circle distance is enough to rank candidates without pulling in a routing service:

from math import radians, sin, cos, asin, sqrt


def haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
    """Great-circle distance in kilometres between two points."""
    lat1, lon1, lat2, lon2 = map(radians, (lat1, lon1, lat2, lon2))
    dlat, dlon = lat2 - lat1, lon2 - lon1
    a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
    return 2 * asin(sqrt(a)) * 6371.0

The distance term feeds directly into the scoring model in the next section. The design principle is that geography is a cost to be minimised, not a gate to be passed — a technician is never excluded for being far, only ranked lower, so a distant specialist remains reachable when proximity has to yield to skill.

The Dispatch Scoring Model

With eligibility filtering removing the ineligible and the distance term quantifying travel cost, the assignment decision becomes a ranking problem. Each surviving candidate receives a composite score that blends three normalised terms: proximity (closer is better), skill margin (a candidate whose credentials exactly match is preferred over one carrying far more than the job needs), and current load (an idle technician is preferred over an overcommitted one). The weights shift with priority — a CRITICAL job weights proximity and speed heavily, while a PLANNED job can weight load-balancing to keep the team evenly utilised.

def score_candidate(tech: Technician, payload: WorkOrderPayload,
                    site_lat: float, site_lon: float) -> float:
    """Higher is better. Blends proximity, load, and priority urgency."""
    distance = haversine_km(tech.home_lat, tech.home_lon, site_lat, site_lon)
    proximity_score = 1.0 / (1.0 + distance)          # 0..1, closer is higher
    load_score = 1.0 / (1.0 + tech.active_jobs)        # 0..1, idler is higher

    # Urgent work weights proximity; planned work weights load balancing.
    if payload.priority in (Priority.CRITICAL, Priority.HIGH):
        weights = (0.7, 0.3)
    else:
        weights = (0.4, 0.6)

    return weights[0] * proximity_score + weights[1] * load_score


def assign_technician(payload: WorkOrderPayload, pool: List[Technician],
                      site_lat: float, site_lon: float,
                      now: datetime) -> Optional[Technician]:
    """Filter to eligible technicians, then return the highest-scoring one."""
    failure_code = _failure_code_for(payload)   # derived from asset + taxonomy
    eligible = [
        t for t in pool
        if is_skill_eligible(t, failure_code, now)
        and is_on_shift(t, now)                  # shift-calendar gate, below
    ]
    if not eligible:
        return None                              # -> review / contractor path
    return max(eligible, key=lambda t: score_candidate(
        t, payload, site_lat, site_lon))

Returning None is a first-class outcome, not an error. An empty eligible pool means no qualified technician is on shift within reach, and that fact must route the work order to a fallback path — a supervisor override, a contractor dispatch, or a held state — rather than forcing a bad assignment. Just as the ingestion pipeline never drops a request it cannot parse, the routing engine never fabricates an assignment it cannot justify.

SLA Timers and Escalation Tiers

An assignment is not enforcement. The requested_completion timestamp defines when a work order is due, but nothing holds the organization to that deadline until an active timer counts down against it and fires escalations at defined thresholds. The SLA timer and escalation subsystem is the enforcement engine: it arms a timer at assignment, evaluates elapsed time against the deadline on each tick, and advances the escalation_tier — paging progressively more senior responders — when thresholds are crossed.

SLA timers are also stateful in a way that trips up naive implementations: the clock does not always run. When a job is blocked waiting on a part, the SLA clock should pause rather than penalise the technician for a delay outside their control, which is why availability checks and parts availability checks feed directly into the timer’s state. A work order deferred because its parts are on order enters a PAUSED state, and the elapsed-time calculation excludes the paused interval when it resumes. Getting this pause-and-resume accounting right is what separates a defensible SLA metric from one that punishes technicians for procurement lead time.

SLA timer states and escalation tiers A work order SLA timer starts in RUNNING when assigned. From RUNNING it can transition to PAUSED when blocked on parts and back to RUNNING when parts arrive, to MET when the work order is completed before the deadline, or to BREACHED when the deadline elapses. A BREACHED timer advances through escalation tiers one, two, and three, paging a lead, a supervisor, and a facilities manager respectively, before reaching RESOLVED. SLA timer states and escalation tiers ASSIGNED arm RUNNING PAUSED parts hold parts arrive MET done in time deadline BREACHED TIER 1 page lead TIER 2 page supervisor TIER 3 page FM RESOLVED
SLA timer as a state machine: RUNNING can pause for a parts hold, complete as MET, or cross its deadline into BREACHED, which then climbs the escalation tiers — lead, supervisor, facilities manager — until the work order is RESOLVED.

The timer evaluates elapsed working time against the deadline and returns the current SLA state along with the tier that breach severity implies. Escalation is driven by how far past the deadline the work order has run, so a marginally late STANDARD job pages a lead while a badly overdue CRITICAL job escalates straight to a facilities manager:

from datetime import datetime, timedelta, timezone


@dataclass
class SLATimer:
    work_order_id: str
    deadline: datetime               # from payload.requested_completion
    paused_seconds: float = 0.0      # accumulated hold time, excluded from SLA
    paused_since: Optional[datetime] = None
    state: str = "RUNNING"


def evaluate_sla(timer: SLATimer, now: datetime) -> tuple[str, int]:
    """Return (state, escalation_tier) for the timer at time `now`."""
    if timer.state in ("MET", "RESOLVED"):
        return timer.state, 0
    if timer.state == "PAUSED":
        return "PAUSED", 0

    # Effective deadline is pushed out by any accumulated pause time.
    effective_deadline = timer.deadline + timedelta(seconds=timer.paused_seconds)
    if now <= effective_deadline:
        return "RUNNING", 0

    overrun = now - effective_deadline
    if overrun <= timedelta(minutes=30):
        return "BREACHED", 1          # tier 1: page the lead
    if overrun <= timedelta(hours=2):
        return "BREACHED", 2          # tier 2: page the supervisor
    return "BREACHED", 3              # tier 3: page the facilities manager


def pause_for_parts(timer: SLATimer, now: datetime) -> None:
    """Stop the SLA clock while a work order is blocked on inventory."""
    if timer.state == "RUNNING":
        timer.state = "PAUSED"
        timer.paused_since = now


def resume(timer: SLATimer, now: datetime) -> None:
    """Restart the clock, banking the paused interval so it is not counted."""
    if timer.state == "PAUSED" and timer.paused_since is not None:
        timer.paused_seconds += (now - timer.paused_since).total_seconds()
        timer.paused_since = None
        timer.state = "RUNNING"

Because the effective deadline is derived by adding banked pause time to the original requested_completion, the SLA metric stays honest: a technician is measured on time they could actually work, and a breach genuinely reflects a missed commitment rather than a procurement delay. The escalation tier this function returns is the same integer that lives on the WorkOrderPayload, so the routing engine and the audit log speak one vocabulary about severity.

Shift-Calendar Availability: Never Dispatch to an Off-Shift Technician

The final eligibility gate is time. A technician who is perfectly qualified and perfectly located is still not a valid assignment if their shift ended two hours ago. Availability is not a flag on the technician record — it is a query against the shift calendar evaluated at the exact moment of dispatch, accounting for rotations, on-call windows, breaks, and approved leave. The shift-calendar integration subsystem is what turns a roster into a real-time availability predicate, and it is the guard that prevents after-hours misfires.

Shift gating also interacts with SLA math in a subtle way. If a CRITICAL job arrives at 3 a.m. and the entire day-shift team is off, the engine must not simply fail — it must consult the on-call rotation, and if the SLA cannot be met within on-call capacity, escalate immediately rather than waiting for the deadline to pass. The same permission model that governs who may even see a work order applies here: an assignment must respect the security access boundaries that determine which technicians and supervisors are authorised for a given site and trade, so routing never assigns a job to someone the access model would forbid from touching it.

The shift predicate is intentionally simple to call and strict to satisfy — a technician is on shift only if the current time falls inside an active, non-leave window for them:

@dataclass
class ShiftWindow:
    technician_id: str
    start: datetime
    end: datetime
    kind: str = "regular"    # "regular" | "on_call" | "leave"


def is_on_shift(tech: Technician, now: datetime,
                calendar: Dict[str, List[ShiftWindow]] = None) -> bool:
    """True only if `now` falls inside an active, non-leave window."""
    windows = (calendar or {}).get(tech.technician_id, [])
    for w in windows:
        if w.kind == "leave":
            continue
        if w.start <= now <= w.end:
            return True
    return False

By expressing availability as a predicate over live calendar data rather than a static attribute, the engine guarantees that no assignment can be made to a technician who is not actually working — closing the after-hours-misfire gap that undermines automated dispatch. The cadence of preventive work that populates those shifts in the first place is itself governed by PM interval calculation, so routine assignments and reactive dispatch draw on the same availability model.

Compliance Audit Logging: Making Every Assignment Provable

Everything above produces decisions; compliance logging makes those decisions defensible. A regulator, an insurer, or an internal auditor may ask, months later, to see that a specific safety-critical work order was routed to a qualified technician and completed within its SLA — and the only acceptable answer is a tamper-evident record of every state transition. The compliance audit logging subsystem emits one immutable entry per event: the assignment decision and the score that justified it, every SLA timer transition, every pause and resume, and every escalation with the responder who was paged.

Audit logging is not the same as operational logging. An operational log is for debugging and can be rotated away; a compliance log is a legal record that must be append-only, hash-chained so any tampering is detectable, and retained for the full statutory period. Each entry references the work_order_id and correlation_id so an auditor can reconstruct a work order’s entire lifecycle — from the moment ingestion committed the payload through assignment, escalation, and closure — as a single ordered narrative. This is what turns compliance from an assertion into a proof.

import hashlib
import json
from datetime import datetime, timezone


def audit_entry(prev_hash: str, event: str,
                work_order_id: str, detail: dict) -> dict:
    """Append-only, hash-chained audit record for one routing event."""
    body = {
        "work_order_id": work_order_id,
        "event": event,                 # "ASSIGNED" | "PAUSED" | "ESCALATED" ...
        "detail": detail,
        "recorded_at": datetime.now(timezone.utc).isoformat(),
        "prev_hash": prev_hash,
    }
    serialized = json.dumps(body, sort_keys=True).encode("utf-8")
    body["entry_hash"] = hashlib.sha256(serialized).hexdigest()
    return body

Because each entry embeds the hash of its predecessor, the log forms a chain in which altering any past record invalidates every record after it — the property auditors rely on to trust that a maintenance history was not edited after the fact. With this in place, the routing engine does not merely make good assignments; it can prove it did, which is the difference between passing an ISO 55001 audit and failing one despite doing the work correctly.

Frequently Asked Questions

How does routing decide between a closer technician and a more qualified one?

Skill eligibility is a hard filter, not a weight: an unqualified technician is removed from the pool before scoring, so proximity can never override competence. Among the technicians who all clear the certification gate, the scoring model blends proximity and current load, weighted by the work order’s priority. That means a nearby generalist never beats the required specialist, but among equally qualified candidates the closer, less-loaded one wins. The mechanics are detailed in skill-based dispatch.

What happens when an SLA is about to breach and no one is available?

The SLA timer escalates on how far past the effective deadline a work order has run, advancing the escalation_tier to page progressively more senior responders — a lead, then a supervisor, then a facilities manager. If the eligible pool is empty at assignment time because everyone qualified is off-shift, the engine returns no assignment and routes the work order to an on-call or contractor path rather than fabricating an invalid one. The escalation ladder is covered in SLA timer and escalation.

Does the SLA clock keep running when a job is waiting on parts?

No. When a work order is blocked pending inventory, the timer moves to a PAUSED state and banks the elapsed hold time, which is then excluded from the SLA calculation when work resumes. The effective deadline is the original requested_completion plus any accumulated pause interval, so a technician is measured only on time they could actually work. This ties directly into parts availability checks, which signal when a job should pause and resume.

How does the engine avoid dispatching to off-shift technicians?

Availability is evaluated as a live predicate against the shift calendar at the moment of dispatch, not read from a static field on the technician record. A technician is eligible only if the current time falls inside an active, non-leave window, and off-shift or on-leave technicians are filtered out before scoring. For after-hours work the engine consults the on-call rotation specifically. The full model lives in shift-calendar integration.

How is SLA compliance proven to an auditor after the fact?

Every routing event — the assignment and its justifying score, each timer transition, every pause and resume, and every escalation — is written to an append-only, hash-chained audit log keyed by work_order_id and correlation_id. Because each entry embeds its predecessor’s hash, any later edit is detectable, so an auditor can reconstruct and trust a work order’s full lifecycle. This is the subject of compliance audit logging.

Build each subsystem with its component guide: skill-based dispatch for certification matching, geographic zone routing for drive-time control, SLA timer and escalation for deadline enforcement, shift-calendar integration for availability gating, and compliance audit logging for provable records. This domain consumes the output of the work order ingestion pipeline and honors the shape defined by work order schema standards.

Part of the CMMS Automation knowledge base.