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.
Root Cause Analysis
The defect sits entirely in how the SLA monitor computes elapsed time, not in the hold workflow itself.
- 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. - On-hold transitions are logged as status changes, not as clock events. The work order service writes
status=open->on_holdandstatus=on_hold->opento 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. - There is no data model for a paused interval. Even if the monitor wanted to subtract hold time, there is nowhere to store
hold_startandhold_endas 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.
Related
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.