Debugging an Expired-Certification Dispatch to an EPA 608 Work Order
A regulated work order does not just need a technician who can plausibly do the job — it needs one whose license or certification is currently valid for that exact trade. This page walks through a production incident where the skill-based dispatch selector inside the broader Technician Routing & SLA Enforcement pipeline dispatched a technician to a refrigerant-recovery job on a lapsed EPA 608 card, because the scorer matched trade labels on string similarity and treated certification “presence” as good enough. Nothing here concerns whether the technician was skilled — it concerns whether the dispatch decision was legal, and why a passing score slipped a compliance failure straight into the field.
Incident Profile
Chiller CHLR-014 needed refrigerant recovered before a compressor swap — a job that, under the Clean Air Act’s Section 608 rules, only a technician holding a current EPA 608 Type II certification may legally perform. The work order intake form carried the free-text trade description "Refrigerant Handling". The dispatch scorer matched it against technician TECH-118, whose profile stored the free-text trade "HVAC", and confirmed an EPA-608 certification was present on file. It dispatched. The technician arrived, checked the card in his wallet against the work order, and refused the job — the card had lapsed ten weeks earlier.
2026-07-14 09:04:11,203 INFO [dispatch_scorer] work_order=WO-2026-1187 required_trade="Refrigerant Handling" asset=CHLR-014
2026-07-14 09:04:11,207 DEBUG [dispatch_scorer] candidate=TECH-118 trade="HVAC" similarity=0.71 (threshold=0.60) MATCH
2026-07-14 09:04:11,209 DEBUG [dispatch_scorer] candidate=TECH-118 certifications=['EPA-608'] present=True
2026-07-14 09:04:11,210 INFO [dispatch_scorer] selected technician=TECH-118 score=0.71
2026-07-14 09:04:11,211 INFO [routing_engine] dispatched WO-2026-1187 to TECH-118
2026-07-14 11:22:47,880 ERROR [field_ops] TECH-118 refused refrigerant recovery on CHLR-014 — EPA 608 card expired 2026-05-02 (73 days lapsed). Work order returned to queue. Compliance incident opened: INC-4471.
The symptom signature has two tells, and both are silent in the happy-path log:
similarity=0.71is a lexical score between two free-text strings,"Refrigerant Handling"and"HVAC"— it is not a match against a controlled trade taxonomy, and a threshold of0.60accepts pairs that share no regulatory meaning at all.present=Truereports only that anEPA-608certification record exists on the technician’s file, with no comparison against its expiry date. A card that lapsed in May reads identically to one valid through next year.
The work order missed its window, the chiller stayed offline for an extra shift while a second technician was found, and the compliance team had to log INC-4471 as a documented licensing violation — the kind of finding that surfaces in an audit regardless of whether the recovery was ever actually attempted.
Root Cause Analysis
Two independent defects had to line up for this dispatch to happen, and neither would have been enough alone.
- Trade matching ran on free-text similarity instead of a controlled taxonomy. The scorer computed a lexical similarity ratio between the work order’s
required_tradestring and each technician’s storedtradestring, then accepted any pair above a fixed threshold."HVAC"and"Refrigerant Handling"share enough characters and tokens to clear0.60even thoughHVAC-GEN(general HVAC) andHVAC-REFRIG(EPA-608-gated refrigerant work) are entirely different regulatory scopes. There was no canonical trade code on either side of the comparison, so the match was never checking the thing that actually mattered. - Certification lookup checked presence, not validity. The scorer asked “does a record for
EPA-608exist on this technician” and stopped there. It never readexpires_on, so a card that lapsed 73 days earlier scored identically to one valid for another year. A regulated certification is only meaningful as a validity window — issued, current, or expired — and this code path collapsed that window into a boolean.
Either defect alone would have been survivable. A correct taxonomy match against an expired-but-present certification would still have selected TECH-118, because presence-only logic doesn’t distinguish expired from current. A correct expiry check against a fuzzy-matched trade would still have selected him, because the trade match never should have succeeded in the first place. It took both gaps open at once to let an out-of-scope, out-of-compliance technician reach dispatch with a passing score. The same class of gap shows up on the read side of security & access boundaries: a role check that confirms a permission exists without confirming it is still granted has the identical shape as a certification check that confirms a record exists without confirming it is still valid.
Resolution
The fix replaces string-similarity trade matching with a lookup against a canonical trade taxonomy, and replaces presence-only certification checks with a validity-window comparison against the dispatch moment.
Before — fuzzy trade match, presence-only certification check:
from difflib import SequenceMatcher
def match_technician(required_trade, technicians):
best = None
best_score = 0.0
for tech in technicians:
# BUG: comparing free-text labels, not controlled trade codes.
similarity = SequenceMatcher(
None, required_trade.lower(), tech["trade"].lower()
).ratio()
if similarity > 0.60 and similarity > best_score:
# BUG: existence only — never checks expires_on.
has_cert = any(
c["code"] == "EPA-608" for c in tech.get("certifications", [])
)
if has_cert:
best = tech
best_score = similarity
return best
After — canonical trade mapping plus a certification validity window:
from datetime import datetime, timezone
from typing import Optional
# Controlled taxonomy: free-text intake labels resolve to one code each.
CANONICAL_TRADE_MAP = {
"refrigerant handling": "HVAC-REFRIG",
"hvac": "HVAC-GEN",
"electrical": "ELEC-LIC",
"plumbing": "PLUMB-LIC",
}
# Regulated trades that require a specific certification code to be current.
REQUIRED_CERT_BY_TRADE = {
"HVAC-REFRIG": "EPA-608",
"ELEC-LIC": "STATE-ELEC-LIC",
}
def canonicalize_trade(raw_trade: str) -> Optional[str]:
"""Resolve free text to a controlled trade code, or None if unresolved."""
return CANONICAL_TRADE_MAP.get(raw_trade.strip().lower())
def certification_is_valid(cert: dict, at: datetime) -> bool:
"""A certification only counts inside its issued/expiry validity window."""
return cert["issued_on"] <= at <= cert["expires_on"]
def match_technician(required_trade, technicians, at=None):
at = at or datetime.now(timezone.utc)
trade_code = canonicalize_trade(required_trade)
if trade_code is None:
# An unresolved taxonomy label is never guessed at by string similarity.
return None
required_cert = REQUIRED_CERT_BY_TRADE.get(trade_code)
for tech in technicians:
if trade_code not in tech.get("trade_codes", []):
continue
if required_cert:
matching = [
c for c in tech.get("certifications", [])
if c["code"] == required_cert
]
if not any(certification_is_valid(c, at) for c in matching):
continue # cert missing or expired -> not eligible
return tech
return None # caller routes to the manual review queue
Three changes carry the fix. canonicalize_trade refuses to guess: an intake label that does not resolve to a controlled code returns None immediately rather than falling back to a fuzzy comparison. The trade check itself becomes an exact membership test against trade_codes, so a general HVAC technician can never satisfy a refrigerant-specific requirement by lexical accident. And certification_is_valid compares the full issued_on–expires_on window against the dispatch moment, so a lapsed card is excluded exactly the same way a missing one is — both return None and let the caller escalate instead of dispatching.
Minimal Reproducible Pipeline
This script runs as-is. It defines the canonical WorkOrderPayload (carrying the mandatory SLA fields priority, requested_completion, and escalation_tier, plus the required_trade this incident turns on), a Technician model with dated certifications, and a dispatch function that falls through to a review queue rather than ever guessing.
import logging
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from enum import Enum
from typing import Dict, List, Optional
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s [%(name)s] %(message)s")
logger = logging.getLogger("cert_trade_matcher")
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
required_trade: 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 Certification:
cert_code: str
issued_on: datetime
expires_on: datetime
authority: str = ""
def is_valid(self, at: datetime) -> bool:
return self.issued_on <= at <= self.expires_on
@dataclass
class Technician:
technician_id: str
trade_codes: List[str] = field(default_factory=list)
certifications: List[Certification] = field(default_factory=list)
CANONICAL_TRADE_MAP = {
"refrigerant handling": "HVAC-REFRIG",
"hvac": "HVAC-GEN",
"electrical": "ELEC-LIC",
"plumbing": "PLUMB-LIC",
}
REQUIRED_CERT_BY_TRADE = {
"HVAC-REFRIG": "EPA-608",
"ELEC-LIC": "STATE-ELEC-LIC",
}
def canonicalize_trade(raw_trade: str) -> Optional[str]:
return CANONICAL_TRADE_MAP.get(raw_trade.strip().lower())
def dispatch_work_order(
wo: WorkOrderPayload,
technicians: List[Technician],
review_queue: List[str],
at: Optional[datetime] = None,
) -> Optional[str]:
"""Return the eligible technician id, or push to review_queue and return None."""
at = at or datetime.now(timezone.utc)
trade_code = canonicalize_trade(wo.required_trade)
if trade_code is None:
review_queue.append(wo.work_order_id)
logger.warning("Unresolved trade %r for %s -> review queue", wo.required_trade, wo.work_order_id)
return None
required_cert = REQUIRED_CERT_BY_TRADE.get(trade_code)
for tech in technicians:
if trade_code not in tech.trade_codes:
continue
if required_cert:
held = [c for c in tech.certifications if c.cert_code == required_cert]
if not any(c.is_valid(at) for c in held):
continue # expired or missing -> not eligible for this technician
logger.info("Dispatching %s to %s (trade=%s)", wo.work_order_id, tech.technician_id, trade_code)
return tech.technician_id
review_queue.append(wo.work_order_id)
logger.warning("No compliant technician for %s (trade=%s) -> review queue", wo.work_order_id, trade_code)
return None
if __name__ == "__main__":
now = datetime(2026, 7, 14, 9, 4, tzinfo=timezone.utc)
work_order = WorkOrderPayload(
work_order_id="WO-2026-1187",
asset_id="CHLR-014",
part_skus=["FILT-DRIER-22"],
required_quantities={"FILT-DRIER-22": 1},
location_id="SITE-04",
required_trade="Refrigerant Handling",
priority=Priority.CRITICAL,
requested_completion=now + timedelta(hours=4),
escalation_tier=1,
)
technicians = [
# Expired card: fails gate 4, must not be dispatched.
Technician(
technician_id="TECH-118",
trade_codes=["HVAC-GEN", "HVAC-REFRIG"],
certifications=[
Certification("EPA-608", now - timedelta(days=800), now - timedelta(days=73), "EPA"),
],
),
# Current card and the right trade code: the correct match.
Technician(
technician_id="TECH-204",
trade_codes=["HVAC-REFRIG"],
certifications=[
Certification("EPA-608", now - timedelta(days=200), now + timedelta(days=300), "EPA"),
],
),
]
review_queue: List[str] = []
selected = dispatch_work_order(work_order, technicians, review_queue, at=now)
print("selected:", selected)
print("review_queue:", review_queue)
Running it prints selected: TECH-204 — the technician whose EPA-608 certification is current wins, and TECH-118 is skipped silently by the gate rather than dispatched and refused in the field. Change TECH-204’s certification to another expired entry and rerun: selected becomes None and review_queue gains WO-2026-1187, which is exactly the deferral behaviour a critical work order should get when no compliant technician exists, feeding the same SLA timer & escalation path that a stale or short dispatch triggers elsewhere in the pipeline.
Prevention Checklist
FAQ
Why did a 0.71 similarity score feel “close enough” to pass?
Because the threshold was tuned against convenience, not regulation. A 0.60 cutoff was chosen to tolerate typos and abbreviations in free-text trade entry, which is a reasonable goal for a search box and an unacceptable one for a compliance gate. The fix does not raise the threshold — it removes string similarity from the regulated path entirely and replaces it with an exact match against a controlled trade code.
Why check the certification’s issue date as well as its expiry?
A validity window catches both directions of error: a card that has lapsed (now past expires_on) and a card entered into the system before it was actually issued, which happens when a renewal record is created early with a placeholder date. Comparing the full window rather than only the expiry catches a bad data entry the same way it catches a genuine lapse.
How does this interact with role-based access control for the dispatch service itself?
They are separate gates that share a shape. Security & access boundaries controls whether the dispatch service is even allowed to read and write technician records; the certification-validity gate in this page controls whether a specific technician is qualified for a specific job once that data is in hand. A caller with full read access can still be handed a None result because no one currently qualifies — permission to look is not the same as eligibility to be dispatched.
What happens to a work order that falls into the manual review queue?
It stays out of automated dispatch entirely. A human dispatcher resolves it — by confirming a technician’s card status directly with the issuing authority, requesting an emergency certification renewal, or holding the job until a qualified technician comes on shift — and the resolution is written back with the same trade code and certification reference the automated matcher would have used, so the audit trail reads consistently regardless of which path made the decision.
Related
This incident is a compliance failure inside skill-based dispatch; see the parent guide for the full scoring and eligibility design, cross-check the read scope with security & access boundaries, trace the downstream timing impact through SLA timer & escalation, and confirm every dispatch decision is recoverable later via compliance audit logging.
Part of: Skill-Based Technician Dispatch.