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, and datetime modules — chosen deliberately to minimize third-party deserialization risk in a system that processes untrusted public input.
  • jsonschema 4.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.

Deterministic department routing decision sequence A normalized, deadline-stamped payload first hits an emergency freeze gate. If a freeze is active the request is diverted to the frozen_intake queue, held, hashed, and audited. If not, the router tries an exact keyword match; on a hit it assigns the target department with a reason code and the table version. On a miss it tries a statutory-reference match, which on a hit also assigns the target department. If nothing matches it routes to the explicit fallback department with reason fallback_assignment. Both the matched assignment and the fallback are published idempotently and persistently to the async queue. Normalized payload scored · deadline-stamped Freeze active? Exact keyword match? Statutory ref match? Fallback department reason: fallback_assignment frozen_intake queue held · hashed · audited Assign target department + reason code · table_version Publish to async queue idempotent · persistent no yes no yes yes no

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.

python
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.

python
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.

python
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.

python
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_dept and reason. 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_tier on the emitted DEPARTMENT_ROUTED event equals the value computed upstream, and that the RoutingPayload raises FrozenInstanceError on any mutation attempt — the urgency signal must survive routing untouched.
  • Trace continuity. Assert that the request_id minted at ingestion appears on every audit line the request produces — schema check, routing decision, freeze or dispatch — so a single grep on the correlation ID reconstructs the full assignment lifecycle.
python
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_ROUTED lines whose target_dept no 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_assignment lines 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-8 confusion, 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_DISPATCHED lines sharing a request_id. Fix: gate dispatch on the request_id:target_dept idempotency key against a durable store, logging DISPATCH_DEDUP on 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_ROUTED event 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 into frozen_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.

← Back to all public records automation topics