Department Routing Logic for Public Records & FOIA Intake
Within Intake & Routing Workflows, department routing logic is the deterministic decision layer that converts a normalized, deadline-stamped request into a single, defensible custodial assignment — the moment where an agency commits, on the record, to who owns a statutory obligation. For government engineering teams and the compliance officers who certify their output, the requirement is not classification accuracy in the abstract; it is reproducibility: the same request, replayed against the routing table that was live when it arrived, must always produce the same department, the same reason code, and the same audit trace. This guide walks a production-ready implementation: consuming an immutable priority directive without mutating it, mapping metadata to a target department through a versioned table, dispatching the assignment idempotently to the work queue, and proving correctness with replay and log assertions.
Problem Framing & Statutory Requirement
A misrouted request is a missed deadline waiting to happen. The federal Freedom of Information Act sets a 20-business-day clock on the agency’s substantive determination (5 U.S.C. § 552(a)(6)(A)(i)), and state open-records analogues impose their own — frequently shorter — windows. When a records request lands in the wrong department’s queue, the clock keeps running while the receiving office triages, re-reads, and forwards it; by the time the correct custodian sees it, days of the statutory window are gone. Routing is therefore a compliance control, not a convenience: the decision must be fast, deterministic, and fully reconstructible after the fact.
Three properties follow directly from the statute. First, immutability of the urgency signal: the priority tier computed upstream anchors tolling and SLA math, so the router must consume it without ever altering it — mutating priority mid-stream can invalidate a deadline calculation and convert a tracked request into a silent breach. Second, deterministic assignment from versioned configuration: hardcoded if/elif chains drift, resist audit, and break under the constant reality of government reorganizations, so the mapping from metadata to department must live in version-controlled configuration that can be snapshotted and replayed. Third, an unbroken audit record: every routing decision must emit an append-only event keyed by a correlation ID, capturing the target department, the reason it was chosen, and the table version in force, so a records manager or an inspector general can prove that a given request was assigned correctly under the rules that existed at the time. Those guarantees align with NIST SP 800-53 AU-9 (protection of audit information) and with the chain-of-custody expectations that records-retention standards impose on disclosure workflows.
Prerequisites & Environment Setup
This implementation targets Python 3.11+ and depends only on a small, audit-friendly surface:
- The standard-library
dataclasses,hashlib,uuid,json,logging, anddatetimemodules — chosen deliberately to minimize third-party deserialization risk in a system that processes untrusted public input. jsonschema4.x (or Pydantic 2.x) to validate the inbound payload against a versioned schema before any routing decision runs.- A version-controlled mapping table — YAML or JSON in a reviewed repository, or a read-only database table — that the engine loads and validates at initialization. The detailed schema and precedence rules for this artifact are covered in routing requests to correct departments using departmental mapping tables.
- A write-once audit sink — an append-only Postgres table, or object storage with a retention lock — to satisfy chain-of-custody requirements.
Access requirements: the routing service runs under a least-privilege identity that can read the mapping table and publish to the work queue but cannot edit the table or consume from the dead-letter exchange, consistent with the security boundary configuration for the deployment. The jurisdiction tags the router keys on are defined centrally in the state-law compliance frameworks so that deadline windows and department mappings stay in sync. Validated, integrity-stamped requests arrive here from email & form parsing pipelines, already scored by priority scoring algorithms; this page assumes those upstream stages have run.
Architecture Overview
The router sits between the scoring stage and the work queue. A normalized payload is first checked for an active emergency freeze; if none applies, the engine attempts a deterministic match sequence — exact keyword, then statutory reference — and falls back to a designated default department rather than guessing. The chosen assignment, its reason code, and the table version are stamped onto an audit event and published as an idempotent message to async queue management.
Step-by-Step Implementation
1. Enforce schema and consume the priority directive immutably
Routing cannot execute reliably without structured input. The payload arriving from upstream carries subject-matter keywords, originating-agency identifiers, statutory references, contact metadata, and the priority tier. Before any decision runs, validate the payload against a versioned schema, quarantine any deviation rather than letting it pass silently, and bind the priority tier into a frozen structure so no downstream code path can alter it. Schema drift is one of the most common causes of misassignment, so a validation failure must emit a structured alert and halt — not default to a guess.
import json
import logging
from dataclasses import dataclass, field
from typing import Dict, List, Optional
# Structured JSON audit logger — forwarded to an append-only sink (NIST SP 800-53 AU-9)
audit_logger = logging.getLogger("foia.routing.audit")
@dataclass(frozen=True) # frozen => the priority directive cannot be mutated after triage
class RoutingPayload:
request_id: str # server-generated correlation ID, set at ingestion
subject_keywords: List[str]
statutory_refs: List[str]
originating_agency: Optional[str]
priority_tier: int # immutable directive from Priority Scoring Algorithms
raw_metadata: Dict[str, str] = field(default_factory=dict)
def load_payload(data: dict) -> RoutingPayload:
required = ("request_id", "subject_keywords", "statutory_refs", "priority_tier")
missing = [k for k in required if k not in data]
if missing:
# Schema failure is quarantined, never routed on a guess
audit_logger.critical(json.dumps({"event": "SCHEMA_QUARANTINE", "missing": missing}))
raise ValueError(f"payload failed schema validation: missing {missing}")
return RoutingPayload(
request_id=data["request_id"],
subject_keywords=list(data["subject_keywords"]),
statutory_refs=list(data["statutory_refs"]),
originating_agency=data.get("originating_agency"),
priority_tier=int(data["priority_tier"]),
raw_metadata=dict(data.get("raw_metadata", {})),
)
Expected behavior: a well-formed payload yields a RoutingPayload whose priority_tier is read-only — any attempt to reassign it raises FrozenInstanceError; a payload missing a required field emits a SCHEMA_QUARANTINE audit line and raises before any routing decision is attempted.
2. Map metadata to a department through a versioned table
The routing engine is a stateless function over a version-controlled mapping table, not a tree of hardcoded conditionals. It loads the table at initialization, validates its structure (including that the fallback department actually exists in it), and applies a fixed precedence: exact keyword match, then statutory-reference lookup, then an explicit fallback. The table version travels with every decision so a later replay can reproduce it exactly.
import uuid
from datetime import datetime, timezone
from typing import Tuple
class DepartmentRouter:
def __init__(self, mapping_table: Dict[str, str], fallback_dept: str, table_version: str):
self.mapping_table = mapping_table
self.fallback_dept = fallback_dept
self.table_version = table_version
self._validate_table()
def _validate_table(self) -> None:
if not isinstance(self.mapping_table, dict) or not self.mapping_table:
raise ValueError("mapping table must be a non-empty dictionary")
# A fallback that is not itself a valid department is a silent-misroute hazard
if self.fallback_dept not in self.mapping_table.values():
raise ValueError("fallback department must exist in the mapping table")
def route(self, payload: RoutingPayload) -> Tuple[str, str, str]:
"""Returns (target_department, routing_reason, audit_trace_id)."""
trace_id = str(uuid.uuid4())
target_dept, reason = self.fallback_dept, "fallback_assignment"
try:
# Deterministic precedence: exact keyword first ...
for keyword in payload.subject_keywords:
if keyword in self.mapping_table:
target_dept, reason = self.mapping_table[keyword], f"exact_keyword_match:{keyword}"
break
# ... then statutory reference if no keyword matched
if reason == "fallback_assignment":
for ref in payload.statutory_refs:
if ref in self.mapping_table:
target_dept, reason = self.mapping_table[ref], f"statutory_ref_match:{ref}"
break
audit_logger.info(json.dumps({
"event": "DEPARTMENT_ROUTED",
"trace_id": trace_id,
"request_id": payload.request_id,
"target_dept": target_dept,
"reason": reason,
"priority_tier": payload.priority_tier, # logged, never modified
"table_version": self.table_version,
"ts": datetime.now(timezone.utc).isoformat(),
}))
return target_dept, reason, trace_id
except Exception as exc:
# Any unexpected error degrades to the audited fallback, never to a dropped request
audit_logger.error(json.dumps({
"event": "ROUTING_FAILURE",
"trace_id": trace_id,
"request_id": payload.request_id,
"error": str(exc),
"table_version": self.table_version,
"ts": datetime.now(timezone.utc).isoformat(),
}))
return self.fallback_dept, f"exception_fallback:{type(exc).__name__}", trace_id
Expected behavior: a payload whose keyword matches the table emits a DEPARTMENT_ROUTED line with reason exact_keyword_match:<keyword>; a payload that matches only on a statutory reference emits statutory_ref_match:<ref>; an unmatched payload routes to the fallback with reason fallback_assignment; and every line carries the table_version that produced it.
3. Apply the emergency freeze gate before any assignment
During litigation holds, broker outages, or mid-stream legislative changes, routing must be able to stop cleanly without losing requests. A freeze flag is evaluated at the engine’s entry point: when active, the router acknowledges the inbound payload, diverts it to a frozen_intake holding state, notifies legal counsel and records custodians, and records a tamper-evident hash of the frozen request. Freezes are reversible only through an authorized administrative action, and resumption is itself an audited event.
import hashlib
def route_with_freeze(router: DepartmentRouter, payload: RoutingPayload,
freeze_active: bool, freeze_reason: str = "") -> Tuple[str, str, str]:
if freeze_active:
# Hash the frozen payload so any later tampering with the held request is detectable
frozen_hash = hashlib.sha256(
json.dumps(payload.raw_metadata, sort_keys=True).encode("utf-8")
).hexdigest()
audit_logger.warning(json.dumps({
"event": "INTAKE_FROZEN",
"request_id": payload.request_id,
"freeze_reason": freeze_reason, # statutory justification, captured on the record
"frozen_hash": frozen_hash,
"ts": datetime.now(timezone.utc).isoformat(),
}))
# Diverted, not dropped: held in frozen_intake until an authorized resume
return "frozen_intake", f"emergency_freeze:{freeze_reason}", payload.request_id
return router.route(payload)
Expected behavior: while a freeze is active, every request emits an INTAKE_FROZEN line carrying the statutory justification and a content hash, and is parked in frozen_intake rather than assigned; once the freeze is lifted by an authorized action, the same payloads route normally and a paired resume event records who lifted it and when.
4. Dispatch the assignment idempotently to the work queue
A completed routing decision is published to async queue management as a persistent, idempotent message carrying the request_id, target_dept, priority_tier, and audit_trace_id. Idempotency keys keep a network partition or a consumer restart from creating a duplicate assignment, and a request that spans multiple jurisdictions is flagged for a collaborative workflow rather than being forced into a single department.
def dispatch_assignment(broker, payload: RoutingPayload, target_dept: str,
reason: str, trace_id: str, seen: set) -> None:
# Idempotency key ties one request+department to exactly one published assignment
key = f"{payload.request_id}:{target_dept}"
if key in seen: # durable store in production; set shown for illustration
audit_logger.info(json.dumps({"event": "DISPATCH_DEDUP", "key": key}))
return
multi = len({r.split(":")[-1] for r in payload.statutory_refs}) > 1
broker.send_task(
"foia.assign_department",
args=[{
"request_id": payload.request_id,
"target_dept": target_dept,
"priority_tier": payload.priority_tier,
"audit_trace_id": trace_id,
"reason": reason,
"multi_jurisdiction": multi, # triggers a collaborative handoff, not a forced single assignment
}],
headers={"x-correlation-id": payload.request_id},
delivery_mode=2, # persistent: survives a broker restart
)
seen.add(key)
audit_logger.info(json.dumps({
"event": "ASSIGNMENT_DISPATCHED", "request_id": payload.request_id,
"target_dept": target_dept, "multi_jurisdiction": multi, "trace_id": trace_id,
}))
Expected behavior: a first dispatch publishes a persistent task and emits ASSIGNMENT_DISPATCHED; a repeat dispatch for the same request and department emits DISPATCH_DEDUP and publishes nothing; a request whose statutory references span more than one jurisdiction is published with multi_jurisdiction: true to trigger a collaborative workflow.
Validation & Verification
Treat the router’s compliance guarantees as testable invariants rather than assumptions. Three checks catch most regressions:
- Deterministic replay. Store the routing table snapshot alongside each processed payload, then re-run the historical payload against that exact snapshot and assert it produces the identical
target_deptandreason. This is the core evidence a compliance officer needs to show a request was assigned correctly under the rules in force at the time. - Priority immutability. Assert that the
priority_tieron the emittedDEPARTMENT_ROUTEDevent equals the value computed upstream, and that theRoutingPayloadraisesFrozenInstanceErroron any mutation attempt — the urgency signal must survive routing untouched. - Trace continuity. Assert that the
request_idminted at ingestion appears on every audit line the request produces — schema check, routing decision, freeze or dispatch — so a singlegrepon the correlation ID reconstructs the full assignment lifecycle.
def test_routing_is_replay_deterministic():
table = {"police-bodycam": "PD-RECORDS", "552a": "PRIVACY-OFFICE", "PD-RECORDS": "PD-RECORDS"}
router = DepartmentRouter(table, fallback_dept="PD-RECORDS", table_version="2026.06.01")
payload = load_payload({
"request_id": "REQ-1", "subject_keywords": ["police-bodycam"],
"statutory_refs": ["552a"], "priority_tier": 70,
})
first = router.route(payload)
second = router.route(payload) # same payload, same table snapshot
assert first[:2] == second[:2] # identical department and reason on replay
assert first[1] == "exact_keyword_match:police-bodycam"
For ongoing assurance, run an automated diff between the active mapping table and its previous version on every deployment, so an unexpected department-code change is surfaced for pre-deployment compliance review instead of silently changing who receives live requests.
Troubleshooting & Edge Cases
- Mapping-table drift between versions. A reorganization renames a department and the table is updated, but in-flight requests were routed under the old codes. Diagnosis:
DEPARTMENT_ROUTEDlines whosetarget_deptno longer exists in the current table. Fix: never delete codes in place — add the new mapping, keep the old as an alias for the deadline window, and rely on the deployment-time table diff to flag the change for review before it goes live. - Silent fallback masking a coverage gap. A whole class of requests quietly lands in the fallback department because no keyword or statutory reference matches. Diagnosis: a rising share of
reason: fallback_assignmentlines for a recurring subject. Fix: alert on fallback-rate thresholds per subject category and add explicit mappings; the fallback is a safety net, not a routing strategy. - OCR and encoding artifacts breaking keyword matches. Keywords extracted from scanned submissions via OCR processing pipelines carry ligature errors or
latin-1/utf-8confusion, so an exact-match lookup misses. Diagnosis: fallback assignments for requests whose raw text visibly contains a mappable term. Fix: normalize keywords to NFC Unicode and casefold before lookup, and maintain a small alias map for known OCR confusions rather than loosening to fuzzy matching, which would break determinism. - Duplicate submissions creating double assignments. A requester re-sends the same form, or a redelivery is treated as new, and two assignments are published for one obligation. Diagnosis: two
ASSIGNMENT_DISPATCHEDlines sharing arequest_id. Fix: gate dispatch on therequest_id:target_deptidempotency key against a durable store, loggingDISPATCH_DEDUPon a match. - Litigation-hold conflict at the routing hop. A request under an active hold is routed and dispatched as if it were ordinary. Diagnosis: a
DEPARTMENT_ROUTEDevent for a record flagged in the hold registry. Fix: evaluate the freeze gate against the hold registry maintained by records retention scheduling before any assignment, and treat any hold as a hard divert intofrozen_intake.
Compliance Verification Checklist
FAQ
Why not route with a simple if/elif chain in code instead of a mapping table?
Because routing is a compliance artifact that must be auditable and reproducible, and code branches are neither. A hardcoded chain cannot be snapshotted and replayed to prove how a request was assigned a year ago, it forces a code deployment for every departmental reorganization, and its decision path is invisible to the records and compliance staff who actually own the mapping. A version-controlled table externalizes the policy: it can be reviewed, diffed before deployment, snapshotted alongside each processed request, and certified by non-engineers — while the engine itself stays a small, stable, deterministic function.
How does the router avoid altering the upstream priority tier?
The priority tier arrives inside a frozen dataclass, so the language itself prevents reassignment — any attempt raises FrozenInstanceError. The router reads the tier, logs it on the routing event, and passes it through to the dispatch message unchanged. Keeping urgency immutable matters because tolling and SLA calculations downstream depend on a stable value; if routing could nudge priority, a deadline computation could silently diverge from what was certified at triage.
What happens to requests that match nothing in the mapping table?
They route to an explicit, pre-validated fallback department with the reason code fallback_assignment, never to a dropped request or a random guess. The fallback exists so no request is ever lost, but a persistent stream of fallback assignments for one subject is a signal that the table has a coverage gap. Production deployments alert on fallback rate per subject and add explicit mappings, treating the fallback strictly as a safety net rather than a routing strategy.
How is a request that spans multiple jurisdictions handled?
The dispatcher inspects the statutory references on the payload and, when they resolve to more than one jurisdiction, publishes the assignment with a multi_jurisdiction flag instead of forcing a single-department choice. That flag triggers a collaborative workflow so each jurisdiction’s custodian can act on its portion under its own deadline window, which preserves statutory clarity and avoids one department overreaching into another’s records.
Related
- Intake & Routing Workflows — the parent control plane that hands validated, deadline-stamped requests to the router.
- Priority Scoring Algorithms — the upstream stage that produces the immutable priority tier the router consumes.
- Async Queue Management — the durable queue this router dispatches assignments into.
- Email & Form Parsing Pipelines — the extraction stage that normalizes and integrity-stamps payloads before routing.
- Routing requests to correct departments using departmental mapping tables — the mapping-table schema, precedence weighting, and configuration patterns behind step 2.
← Back to all public records automation topics