Fixing False SLA Breaches When Work Orders Are On Hold for Parts on Order

A work order that is legitimately waiting on a backordered part should not count against a technician’s response time, yet that is exactly what happens when SLA timer and escalation logic treats every second between creation and completion as billable against the deadline. The broader technician routing and SLA enforcement system depends on the clock reflecting only time the technician could actually have acted — and a job parked in a “waiting for parts” state for two days is not two days of dispatch failure. This page walks through the incident, the timer defect that causes it, and a paused-interval model that fixes it without touching escalation policy elsewhere.

Incident Profile

The SLA monitor flagged a standard-tier work order as breached at nearly double its allotted window. The audit log shows the job spent almost a full day sitting in on_hold waiting for a seal to arrive, and the monitor never accounted for that gap:

2026-06-28 10:00:00 INFO    [work_order_service] WO-55219 created status=open priority=standard sla_hours=24
2026-06-28 16:00:00 INFO    [work_order_service] WO-55219 status=open->on_hold reason=awaiting_part sku=SEAL-2210 required_qty=1
2026-06-29 12:00:00 INFO    [work_order_service] WO-55219 status=on_hold->open reason=part_received sku=SEAL-2210
2026-06-30 02:00:00 WARNING [sla_monitor] WO-55219 elapsed=40h00m due=24h00m breach=True escalation_tier=1->2
2026-06-30 02:00:01 ERROR   [escalation_engine] WO-55219 escalated to on-call supervisor; technician penalized for SLA miss

The math the monitor used is elapsed = now - created_at, giving 40 hours against a 24-hour SLA — a clean breach on paper. But the job was only actually open and actionable for 6 hours before the hold, and another 14 hours after the part arrived: 20 hours of real elapsed time, comfortably inside the 24-hour window. The technician did nothing wrong; a supplier lead time did. The hold itself was created correctly by the same logic that drives a parts availability check — the part was out of stock, a reorder fired through automated reorder triggers, and the work order was parked until receipt. Every part of that workflow behaved as designed. Only the SLA clock failed to notice.

Wall-clock timeline versus naive and paused-adjusted SLA elapsed time A timeline shows the work order running from creation, entering a paused state when the part is placed on order, and resuming when the part is received, ending at a breach check 40 hours after creation. Below, a naive elapsed bar measures the full 40 hours from created_at to now and crosses the 24-hour SLA due threshold, triggering a false breach. A corrected elapsed bar subtracts the 20-hour paused interval, measuring only 20 hours of actual running time, which stays under the 24-hour threshold and is not breached. Why the timer breaches a job that was legitimately on hold Wall-clock timeline running paused (awaiting part) running created_at 0h hold_start part ordered · 6h hold_end part received · 26h breach check now · 40h SLA due · 24h Naive: elapsed = now − created_at (wrong) 40h elapsed BREACH (false) Corrected: elapsed = wall time − paused time 20h elapsed within SLA clock running clock paused (on-order hold) SLA due threshold

Root Cause Analysis

The defect sits entirely in how the SLA monitor computes elapsed time, not in the hold workflow itself.

  1. Elapsed time is a raw subtraction, never a paused-aware one. The monitor’s only formula is elapsed = now - created_at. There is no concept of time that does not count, so every hold — no matter the reason — silently accrues against the deadline.
  2. On-hold transitions are logged as status changes, not as clock events. The work order service writes status=open->on_hold and status=on_hold->open to the audit trail, which is exactly the kind of record a compliance and audit log needs — but nothing downstream translates those two log lines into a pause and resume for the timer. The status field and the SLA clock are two systems that never talk to each other.
  3. There is no data model for a paused interval. Even if the monitor wanted to subtract hold time, there is nowhere to store hold_start and hold_end as a discrete, queryable interval. Without that structure, “how long was this job actually paused” is not a question the system can answer, so it defaults to answering “zero.”

The fix does not change when a job goes on hold or why — that decision correctly belongs to inventory and reorder logic. It changes what the SLA timer does with the fact that a hold happened.

Resolution

The corrected model introduces a PauseInterval record with an open (start, no end) or closed (start, end) state, driven directly from the same on_hold / resume transitions the work order service already emits. Elapsed time becomes wall-clock time minus the sum of all paused durations.

Before — naive elapsed, no concept of a pause:

from datetime import datetime, timedelta, timezone


class NaiveSLATimer:
    """Buggy: counts on-hold time as if the technician were still on the clock."""

    def __init__(self, created_at: datetime, sla_hours: float):
        self.created_at = created_at
        self.sla_hours = sla_hours

    def elapsed(self, now: datetime = None) -> timedelta:
        now = now or datetime.now(timezone.utc)
        # No adjustment for on_hold intervals -> waiting-for-parts time is billed.
        return now - self.created_at

    def is_breached(self, now: datetime = None) -> bool:
        return self.elapsed(now) > timedelta(hours=self.sla_hours)

After — paused intervals subtracted from wall time:

from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from typing import List, Optional


@dataclass
class PauseInterval:
    """One waiting-for-parts hold: open while end is None."""
    start: datetime
    end: Optional[datetime] = None

    def duration(self, as_of: datetime) -> timedelta:
        return (self.end or as_of) - self.start


@dataclass
class SLAClock:
    created_at: datetime
    sla_hours: float
    pauses: List[PauseInterval] = field(default_factory=list)

    def pause(self, at: Optional[datetime] = None) -> None:
        """Call when status transitions to on_hold (e.g. part placed on order)."""
        at = at or datetime.now(timezone.utc)
        if self.pauses and self.pauses[-1].end is None:
            return  # already paused; ignore duplicate hold events
        self.pauses.append(PauseInterval(start=at))

    def resume(self, at: Optional[datetime] = None) -> None:
        """Call when status transitions back to open (e.g. part received)."""
        at = at or datetime.now(timezone.utc)
        if self.pauses and self.pauses[-1].end is None:
            self.pauses[-1].end = at

    def total_paused(self, as_of: Optional[datetime] = None) -> timedelta:
        as_of = as_of or datetime.now(timezone.utc)
        return sum((p.duration(as_of) for p in self.pauses), timedelta())

    def elapsed(self, as_of: Optional[datetime] = None) -> timedelta:
        as_of = as_of or datetime.now(timezone.utc)
        wall_time = as_of - self.created_at
        # Subtract every paused interval, including one still open right now.
        return wall_time - self.total_paused(as_of)

    def is_breached(self, as_of: Optional[datetime] = None) -> bool:
        return self.elapsed(as_of) > timedelta(hours=self.sla_hours)

The change is small in surface area but changes the answer completely: elapsed now measures only the time the job was actually actionable. A hold that opens and never closes (the part is still on order) keeps counting as paused right up to the moment resume is called — there is no need for a background job to “remember” to stop the clock, because total_paused computes the open interval’s duration against as_of on every call.

Minimal Reproducible Pipeline

This script runs standalone. It defines the canonical WorkOrderPayload with the site-wide SLA fields, drives an SLAClock through a create → hold → resume → breach-check sequence matching the incident above, and prints both the naive and corrected verdicts so you can see the false positive disappear.

from dataclasses import dataclass, field
from datetime import datetime, timedelta, 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))


@dataclass
class PauseInterval:
    start: datetime
    end: Optional[datetime] = None

    def duration(self, as_of: datetime) -> timedelta:
        return (self.end or as_of) - self.start


@dataclass
class SLAClock:
    created_at: datetime
    sla_hours: float
    pauses: List[PauseInterval] = field(default_factory=list)

    def pause(self, at: Optional[datetime] = None) -> None:
        at = at or datetime.now(timezone.utc)
        if self.pauses and self.pauses[-1].end is None:
            return
        self.pauses.append(PauseInterval(start=at))

    def resume(self, at: Optional[datetime] = None) -> None:
        at = at or datetime.now(timezone.utc)
        if self.pauses and self.pauses[-1].end is None:
            self.pauses[-1].end = at

    def total_paused(self, as_of: Optional[datetime] = None) -> timedelta:
        as_of = as_of or datetime.now(timezone.utc)
        return sum((p.duration(as_of) for p in self.pauses), timedelta())

    def elapsed(self, as_of: Optional[datetime] = None) -> timedelta:
        as_of = as_of or datetime.now(timezone.utc)
        return (as_of - self.created_at) - self.total_paused(as_of)

    def naive_elapsed(self, as_of: Optional[datetime] = None) -> timedelta:
        as_of = as_of or datetime.now(timezone.utc)
        return as_of - self.created_at

    def remaining(self, as_of: Optional[datetime] = None) -> timedelta:
        return timedelta(hours=self.sla_hours) - self.elapsed(as_of)

    def is_breached(self, as_of: Optional[datetime] = None) -> bool:
        return self.elapsed(as_of) > timedelta(hours=self.sla_hours)


def hold_for_part_on_order(wo: WorkOrderPayload, clock: SLAClock, sku: str, at: datetime) -> None:
    """Transition to on_hold and pause the clock in the same call — no gap between them."""
    wo.status = "on_hold"
    clock.pause(at)
    print(f"{wo.work_order_id} status=open->on_hold reason=awaiting_part sku={sku}")


def resume_on_receipt(wo: WorkOrderPayload, clock: SLAClock, sku: str, at: datetime) -> None:
    wo.status = "open"
    clock.resume(at)
    print(f"{wo.work_order_id} status=on_hold->open reason=part_received sku={sku}")


if __name__ == "__main__":
    created_at = datetime(2026, 6, 28, 10, 0, tzinfo=timezone.utc)
    work_order = WorkOrderPayload(
        work_order_id="WO-55219",
        asset_id="PUMP-12",
        part_skus=["SEAL-2210"],
        required_quantities={"SEAL-2210": 1},
        location_id="WH-04",
        priority=Priority.STANDARD,
        created_at=created_at,
    )
    clock = SLAClock(created_at=created_at, sla_hours=24)

    hold_start = created_at + timedelta(hours=6)     # part goes on order
    hold_end = created_at + timedelta(hours=26)       # part received
    breach_check = created_at + timedelta(hours=40)   # monitor runs

    hold_for_part_on_order(work_order, clock, "SEAL-2210", hold_start)
    resume_on_receipt(work_order, clock, "SEAL-2210", hold_end)

    print(f"naive elapsed:     {clock.naive_elapsed(breach_check)}")
    print(f"corrected elapsed: {clock.elapsed(breach_check)}")
    print(f"paused total:      {clock.total_paused(breach_check)}")
    print(f"corrected breach:  {clock.is_breached(breach_check)}")
    print(f"time remaining:    {clock.remaining(breach_check)}")

    assert clock.naive_elapsed(breach_check) == timedelta(hours=40)
    assert clock.total_paused(breach_check) == timedelta(hours=20)
    assert clock.elapsed(breach_check) == timedelta(hours=20)
    assert clock.is_breached(breach_check) is False

Running it prints a naive elapsed of 40 hours alongside a corrected elapsed of 20 hours — the same job, two different verdicts, and only the second one is defensible against the technician. Wire hold_for_part_on_order and resume_on_receipt directly into the same event handlers that already fire when a parts availability check finds a shortage and a reorder is placed, so the pause and the status change can never drift apart.

Prevention Checklist

FAQ

Should the clock pause for every hold reason, or only parts-on-order holds?

Only pause for holds outside the technician’s control — parts on order is the clearest case, but a hold for customer access or safety lockout qualifies too. A hold caused by the technician’s own inaction (for example, an unread assignment) should not pause the clock, since that is exactly the delay the SLA exists to catch.

What happens if the part is placed on order, then the reorder itself needs correcting?

The clock stays paused the entire time — a correction inside automated reorder triggers does not reopen the work order, so it never touches the SLA timer. Only a genuine resume event (the part physically arrives) should call SLAClock.resume.

Does pausing the clock also need to pause the escalation tier countdown?

Yes — escalation tiers should key off elapsed, not off wall time, or a job can still climb tiers while legitimately on hold. Route escalation_tier increments through SLAClock.elapsed() and remaining() rather than a separate timestamp comparison so the two never disagree.

Can a work order have overlapping pause intervals?

No, and the pause() guard in this page’s SLAClock enforces that by ignoring a second pause call while one is already open. If a job needs two concurrent hold reasons (for example, awaiting parts and awaiting site access), track them as one logical pause with a combined reason rather than two intervals, since overlapping intervals would double-subtract the same wall-clock time.

This fix sits inside SLA timer and escalation enforcement, the same section that governs how technician routing and SLA enforcement escalates missed jobs; the hold that triggers a pause originates upstream in parts availability checks and automated reorder triggers, and every pause/resume event should be traceable through compliance and audit logging.

Part of: SLA Timers & Escalation Enforcement.