Priority Scoring Algorithms for Government Records Automation
Within Intake & Routing Workflows, priority scoring algorithms are the deterministic layer that turns unstructured request metadata into a defensible queue position before any custodian touches the request. The score executes immediately after Email & Form Parsing Pipelines emit a validated payload, and the tier it produces becomes an immutable directive that Department Routing Logic and Async Queue Management consume downstream. This guide covers the statutory stakes, the environment, a runnable scoring service, how to verify it, and the failure modes that bite government records teams in production.
Problem Framing & Statutory Requirement
A public records program does not get to process requests in the order it prefers. Federal FOIA fixes a 20-business-day response window under 5 U.S.C. § 552(a)(6)(A)(i), most state open-records acts impose their own (often shorter) clocks, and several statutes grant accelerated handling — for example expedited processing under 5 U.S.C. § 552(a)(6)(E) when there is a compelling need or urgency to inform the public. A naive first-in-first-out queue silently breaches the tightest deadlines first, because it has no concept of how much statutory runway each request has left.
Priority scoring solves that by making urgency a quantified, auditable number rather than a custodian’s judgement call. Two compliance properties matter more than raw cleverness here. First, determinism: the same metadata must always yield the same score, because an agency may have to defend a routing decision in litigation or before an oversight body. Second, transparency: every component that contributed to the score, and the exact weight matrix version in force at the time, must be reconstructable from the audit log. A scoring engine that cannot be replayed is a liability, not an asset.
This page treats scoring as a stateless, configuration-driven function. Compliance officers tune the weight matrix; engineers guarantee the function is pure, bounded, and logged. Keeping those concerns separate is what lets you change policy without a code deployment and still prove, months later, why request 2026-004417 was marked critical.
Prerequisites & Environment Setup
- Python 3.11+ — required for
datetime.UTC, thetomllibstandard-library TOML reader (handy for externalized weight matrices), and exception-group ergonomics. - Standard library only for the core:
dataclasses,enum,datetime,json,logging,hashlib. The scoring kernel deliberately takes no third-party runtime dependency so it can run inside any worker without supply-chain risk. hypothesis>=6.100(dev/test only) for property-based validation of the bounds and monotonicity invariants.- A versioned configuration source for the weight matrix — a TOML/JSON file in version control or a read-only config table. The scorer must never read mutable application state.
- Access controls: the scoring worker needs read access to parsed request metadata and append-only write access to the audit sink. It needs nothing else; enforce that boundary per Security Boundary Configuration so a compromised scorer cannot mutate request bodies or routing tables.
Sensitivity flags consumed by the scorer (PII, health, law-enforcement, minor) should already be attached upstream during parsing; the scorer reads them, it does not re-derive them.
Architecture Overview
The scoring stage sits between parsing and routing as a pure function: validated metadata in, an immutable ScoreResult plus one audit line out. It never blocks on I/O beyond emitting that audit record, which keeps it cheap enough to run inline in an ingestion worker or as its own queue-dispatched task.
Step-by-Step Implementation
1. Define the weight schema and statutory factor model
A production scoring algorithm is a weighted additive model where each factor maps to a specific compliance or operational trigger: statutory deadline proximity, requestor classification, content sensitivity, and the target department’s historical processing latency. Each factor carries a normalized weight in 0.0–1.0 and the weights must sum to 1.0, so the final score is the bounded sum , scaled to a fixed 0–100 integer range. Validating the weight sum at construction time turns a misconfigured policy into an immediate, loud failure instead of silent priority drift.
from dataclasses import dataclass, field
from typing import Dict, List
from datetime import datetime, timedelta, timezone
from enum import Enum
class RequestorType(str, Enum):
CITIZEN = "citizen"
MEDIA = "media"
LEGISLATIVE = "legislative"
AGENCY = "agency"
@dataclass(frozen=True)
class ScoringWeights:
# Weights are policy, not code: load these from a versioned config source
# (TOML/JSON in VCS) so compliance can retune without a deployment.
deadline_proximity: float = 0.35
requestor_type: float = 0.20
content_sensitivity: float = 0.25
dept_latency: float = 0.20
version: str = "weights-2026-01" # logged with every score for replay
def __post_init__(self):
total = (
self.deadline_proximity
+ self.requestor_type
+ self.content_sensitivity
+ self.dept_latency
)
# Fail loudly on misconfiguration rather than skewing every score.
if not (0.99 <= total <= 1.01):
raise ValueError(f"Weights must sum to 1.0. Current sum: {total}")
@dataclass(frozen=True)
class RequestMetadata:
request_id: str
received_at: datetime # tz-aware; the statutory clock starts here
statutory_deadline: datetime # precomputed per jurisdiction upstream
requestor_type: RequestorType
sensitivity_flags: List[str]
target_department: str
historical_avg_days: float
Note that statutory_deadline is supplied, not computed here — deadline calculation is jurisdiction-specific (business-day counting, tolling, holiday suspension) and belongs with the rules maintained under State-Law Compliance Frameworks. The scorer only reasons about proximity to a deadline it is handed.
2. Compute deterministic component scores
Each component is a small, side-effect-free method returning a normalized 0.0–1.0 value. Keeping them isolated is what makes the engine testable: you can assert each curve independently and assert their composition separately.
@dataclass
class ScoreResult:
request_id: str
priority_score: int
breakdown: Dict[str, float]
weights_version: str
audit_timestamp: datetime
warnings: List[str] = field(default_factory=list)
class PriorityScorer:
def __init__(self, weights: ScoringWeights):
self.weights = weights
# Media and legislative requests often carry expedited-processing
# exposure (5 U.S.C. § 552(a)(6)(E)); reflect that in the multiplier.
self._type_multipliers = {
RequestorType.MEDIA: 1.0,
RequestorType.LEGISLATIVE: 0.9,
RequestorType.CITIZEN: 0.7,
RequestorType.AGENCY: 0.5,
}
def _deadline_score(self, m: RequestMetadata) -> float:
# 5 U.S.C. § 552(a)(6)(A)(i): the 20-business-day clock. Less runway
# left => higher urgency. An elapsed deadline pins to maximum.
days_remaining = (m.statutory_deadline - m.received_at).days
if days_remaining <= 0:
return 1.0
if days_remaining <= 5:
return 0.9
if days_remaining <= 15:
return 0.6
return 0.2
def _requestor_score(self, m: RequestMetadata) -> float:
return self._type_multipliers.get(m.requestor_type, 0.5)
def _sensitivity_score(self, m: RequestMetadata) -> float:
# Sensitive content needs senior/dedicated handling and review headroom
# before the deadline; weight it up, but keep it bounded.
base = 0.3
for flag in m.sensitivity_flags:
if flag.lower() in ("pii", "health", "law_enforcement", "minor"):
base += 0.25
return min(base, 1.0)
def _latency_score(self, m: RequestMetadata) -> float:
# Departments that historically run slow need an earlier start to land
# inside the same statutory window.
if m.historical_avg_days <= 0:
return 0.5
if m.historical_avg_days > 20:
return 0.9
if m.historical_avg_days > 10:
return 0.6
return 0.3
3. Emit the audited priority score
The compute method composes the components, clamps the result, and — critically — emits a single structured JSON audit line carrying the weight-matrix version, an integrity hash of the input, and every component score. That line is the record an oversight body replays.
import json
import logging
import hashlib
audit_logger = logging.getLogger("priority_scoring_audit")
class PriorityScorer(PriorityScorer): # continued for illustration
def _input_hash(self, m: RequestMetadata) -> str:
canonical = json.dumps(
{
"request_id": m.request_id,
"received_at": m.received_at.isoformat(),
"statutory_deadline": m.statutory_deadline.isoformat(),
"requestor_type": m.requestor_type.value,
"sensitivity_flags": sorted(f.lower() for f in m.sensitivity_flags),
"target_department": m.target_department,
"historical_avg_days": m.historical_avg_days,
},
sort_keys=True,
separators=(",", ":"),
)
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
def compute(self, m: RequestMetadata) -> ScoreResult:
warnings: List[str] = []
try:
components = {
"deadline_proximity": self._deadline_score(m),
"requestor_type": self._requestor_score(m),
"content_sensitivity": self._sensitivity_score(m),
"dept_latency": self._latency_score(m),
}
except Exception:
# Never silently drop a request: log and re-raise so the queue
# routes it to the DLQ rather than losing the obligation.
audit_logger.exception(
json.dumps({"event": "scoring_error", "request_id": m.request_id})
)
raise
raw = (
self.weights.deadline_proximity * components["deadline_proximity"]
+ self.weights.requestor_type * components["requestor_type"]
+ self.weights.content_sensitivity * components["content_sensitivity"]
+ self.weights.dept_latency * components["dept_latency"]
)
priority_score = max(0, min(100, int(raw * 100))) # bounded by contract
if priority_score >= 85:
warnings.append("HIGH_PRIORITY_THRESHOLD_EXCEEDED")
result = ScoreResult(
request_id=m.request_id,
priority_score=priority_score,
breakdown={k: round(v, 3) for k, v in components.items()},
weights_version=self.weights.version,
audit_timestamp=datetime.now(timezone.utc),
warnings=warnings,
)
# Append-only audit line (NIST SP 800-53 AU-9: protect audit info).
audit_logger.info(
json.dumps(
{
"event": "priority_scored",
"request_id": result.request_id,
"input_hash": self._input_hash(m),
"weights_version": result.weights_version,
"priority_score": result.priority_score,
"breakdown": result.breakdown,
"warnings": result.warnings,
"ts": result.audit_timestamp.isoformat(),
}
)
)
return result
Expected output for a media request with five days of runway and a PII flag against a slow department is a high score and a single audit line:
{"event": "priority_scored", "request_id": "2026-004417", "input_hash": "9f2c…", "weights_version": "weights-2026-01", "priority_score": 86, "breakdown": {"deadline_proximity": 0.9, "requestor_type": 1.0, "content_sensitivity": 0.55, "dept_latency": 0.9}, "warnings": ["HIGH_PRIORITY_THRESHOLD_EXCEEDED"], "ts": "2026-06-27T14:02:11.481209+00:00"}
For agencies whose weights must shift with municipal load, Implementing dynamic priority scoring for urgent municipal requests extends this kernel with adaptive recalibration that still respects statutory baselines.
4. Map scores to interoperable priority bands
Routing and SLA enforcement consume bands, not raw integers, so that a request forwarded between agencies keeps a stable meaning. Standardize on fixed, documented bands and pass them downstream as immutable directives:
Because the band is derived once and never recomputed downstream, neither Department Routing Logic nor a worker pool can quietly re-rank a request and invalidate the deadline reasoning that produced it.
Validation & Verification
Deterministic scoring earns its name only if you prove the invariants hold. Treat the kernel like any other safety-critical pure function:
- Bounds invariant (property-based). Generate arbitrary valid metadata with
hypothesisand assert0 <= result.priority_score <= 100always. A failing case is almost always a new sensitivity flag or weight set that broke the clamp assumption. - Determinism / replay. Score the same
RequestMetadatatwice and assert identicalpriority_score,breakdown, andinput_hash. This is the test that lets you defend a routing decision: the audit line is reproducible from the inputs. - Monotonicity. Holding everything else constant, reducing
days_remainingmust never decrease the score. Encode this as a targeted parametric test, not just a property, so regressions name the exact factor. - Log assertion. Capture the
priority_scoring_auditlogger in tests and assert exactly one JSON line percomputecall, that it parses, and that it carriesweights_versionandinput_hash. A scored request with no audit line is a compliance gap, not a passing test.
import json, logging
from hypothesis import given, strategies as st
def test_score_is_always_bounded(caplog):
scorer = PriorityScorer(ScoringWeights())
with caplog.at_level(logging.INFO, logger="priority_scoring_audit"):
r = scorer.compute(sample_metadata()) # build a valid fixture
assert 0 <= r.priority_score <= 100
line = json.loads(caplog.records[-1].message) # exactly-one audit line
assert line["weights_version"] == r.weights_version
Troubleshooting & Edge Cases
- Naive vs. tz-aware datetimes. If
received_atis naive andstatutory_deadlineis tz-aware (or vice versa), the subtraction raisesTypeErrorand the request lands in the DLQ. Fix: normalize every timestamp to UTC at the parsing boundary and reject naive datetimes before they reach the scorer. - Weight matrix drift after a policy edit. A hand-edited config whose factors no longer sum to
1.0either fails construction (good) or, if validation is bypassed, silently compresses the dynamic range so nothing ever reachesCritical. Diagnosis: watch the score distribution; a sudden ceiling well below 86 signals a bad matrix. Fix: gate every weight change through the__post_init__sum check in CI and logweights_versionon every score. - Duplicate submissions inflating apparent load. The same request arriving twice produces two identical scores and can double-book a department’s capacity planning. Fix: dedupe on the canonical
input_hash(or the upstream idempotency key) before scoring, and let Async Queue Management reject the redelivery rather than scoring it again. - Litigation-hold conflict. A request under a litigation or records hold must not be auto-expedited or auto-closed by its score. Fix: treat an active hold as an override that pins the request to a manual review lane regardless of band, and align the audit retention with Records-Retention Scheduling.
- Unknown sensitivity flags. A new flag (e.g.
cjis) the scorer doesn’t recognize is silently ignored, under-scoring genuinely sensitive material. Fix: validate flags against an allowlist at parse time and emit aUNKNOWN_SENSITIVITY_FLAGwarning so the gap surfaces instead of hiding. - Emergency freeze during outages or declared emergencies. During statutory tolling, holiday suspension, or a system outage, bypass scoring entirely with a circuit breaker and route everything to a manual triage lane carrying a
FROZEN_PRIORITYflag, so automated routing cannot misallocate during degraded operation.
Compliance Verification Checklist
FAQ
Why use a deterministic weighted model instead of a machine-learning ranker?
Because the output has to be explainable and replayable. An agency may be asked — in litigation or by an oversight body — to justify exactly why one request was prioritized over another. A weighted additive model reconstructs that answer from the logged weight-matrix version and component breakdown; a learned ranker generally cannot, and it risks encoding bias that conflicts with the statutory duty to treat requesters even-handedly. Determinism is a compliance feature here, not a limitation.
How do compliance officers change priorities without a code deployment?
The weight matrix is configuration, not code. Officers edit a versioned TOML/JSON file (or config table), the change passes the sum-to-1.0 validation in CI, and the scorer picks up the new version string, which is then stamped on every subsequent audit line. Because scores carry their weights_version, you can always tell which policy produced a given decision and replay it. The computation logic stays frozen while policy moves.
Where does the statutory deadline itself come from if the scorer doesn't compute it?
Deadline calculation — business-day counting, holiday suspension, and tolling — is jurisdiction-specific and lives with the rules in State-Law Compliance Frameworks. Those rules run upstream and attach a tz-aware statutory_deadline to the parsed metadata. The scorer only reasons about proximity to that deadline, which keeps the urgency curve uniform across jurisdictions while the hard date stays authoritative and separately auditable.
Related
- Intake & Routing Workflows — the parent control plane this scoring stage feeds.
- Email & Form Parsing Pipelines — the upstream stage that delivers the validated metadata scored here.
- Department Routing Logic — the downstream consumer of the priority band as an immutable directive.
- Async Queue Management — routes and dedupes scored requests by tier into isolated worker pools.
- Implementing dynamic priority scoring for urgent municipal requests — adaptive weight recalibration on top of this kernel.
← Back to all public records automation topics