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.71 is 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 of 0.60 accepts pairs that share no regulatory meaning at all.
  • present=True reports only that an EPA-608 certification 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.

Certification-to-trade validation ladder A four-gate decision ladder a dispatch match must pass in order: canonicalize the free-text trade to a controlled code, confirm the technician holds that trade code, confirm a certification record for the required cert code exists, and confirm that certification's validity window covers the dispatch moment. Failing any gate routes the candidate to a manual review queue instead of dispatch. A callout above the ladder shows the legacy bug: the old scorer matched on trade-string similarity and cert presence only, skipping gates three and four entirely, so an expired certification still reached dispatch. Trade-to-certification validation ladder Legacy bug: string similarity + cert presence only — gates 3 and 4 never ran "HVAC" vs "Refrigerant Handling" scored 0.71 similarity; an on-file EPA-608 record counted regardless of its expiry date Gate 1 · Canonicalize trade free-text "Refrigerant Handling" -> canonical code HVAC-REFRIG Gate 2 · Trade code held technician's trade_codes must contain HVAC-REFRIG, not a fuzzy relative Gate 3 · Certification exists a record for cert_code EPA-608 must be present on the technician Gate 4 · Validity window covers dispatch time issued_on <= now <= expires_on, checked against a timezone-aware clock Manual review queue any failed gate lands here, never dispatch Dispatch confirmed all four gates passed legacy shortcut

Root Cause Analysis

Two independent defects had to line up for this dispatch to happen, and neither would have been enough alone.

  1. 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_trade string and each technician’s stored trade string, then accepted any pair above a fixed threshold. "HVAC" and "Refrigerant Handling" share enough characters and tokens to clear 0.60 even though HVAC-GEN (general HVAC) and HVAC-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.
  2. Certification lookup checked presence, not validity. The scorer asked “does a record for EPA-608 exist on this technician” and stopped there. It never read expires_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_onexpires_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.

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.