Skip to content

FDA 24-Hour Response Automation for FSMA 204 Recalls

When an FDA investigator issues a records request during an outbreak, the clock that matters is not measured in days. Under FSMA 204, a facility that holds records for a food on the Traceability List must provide an electronic, sortable spreadsheet of the requested traceability information within 24 hours of the request, or within a reasonable time the agency agrees to. That obligation is the sharpest edge of the whole Recall Simulation & FDA 24-Hour Response program: every upstream investment in clean Key Data Elements exists so that this one deliverable can be assembled, formatted, and handed over before the deadline expires. This guide owns the orchestration that turns a request into a compliant file on time, every time.

The problem is deceptively narrow but operationally brutal. A request names a product, a lot, or a window, and the response must reconstruct the relevant portion of the trace graph, project it into the exact column layout the FDA expects, and deliver it as a sortable electronic spreadsheet — all while a stopwatch runs. Doing this by hand invites missed deadlines and inconsistent files; doing it as one monolithic script invites a single slow query that silently eats the entire budget. The answer is a staged orchestrator with an explicit timing budget per stage, hard deadlines enforced in code, and observability that tells an operator, mid-incident, exactly how much of the 24 hours remains. This page builds that orchestrator on top of the sibling lot-level recall scoping and one-up, one-back reconstruction guides, and wires its evidence trail into the audit-log export.

Response Workflow and SLA Budget

The response runs as a fixed sequence of five stages, each with its own time budget carved out of the 24-hour window. The pipeline is: request intake (parse and authenticate the FDA request, resolve it to concrete identifiers), scope (expand the request to the full set of implicated Traceability Lot Codes), reconstruct (walk the one-up/one-back chain to gather every relevant Critical Tracking Event), format (project the reconstructed records into the FDA sortable-spreadsheet column layout), and deliver (write the file, compute its hash, and register the delivery artifact). Treating these as discrete stages is what makes the deadline enforceable: each stage is timed independently, and an overrun is caught the moment a stage exceeds its slice rather than at the end when nothing can be done.

The budgets are deliberately conservative and sum to well under 24 hours, because the buffer is not slack — it is the margin an operator needs to rerun a stage after an FDA clarification, absorb a slow database, or handle a partial trace gap surfaced by the Fallback Routing Logic. Reconstruction is given the largest slice because full-chain graph traversal under production data volumes is the stage most sensitive to load; intake and delivery are cheap. The orchestrator records the wall-clock cost of every stage so that repeated drills, run through the mock recall drills program, produce a benchmark distribution rather than a single anecdote. When the p95 of any stage creeps toward its budget, that is the early warning that the query layer needs indexing work long before a real request arrives.

Five-stage SLA budget inside the 24-hour window The response runs as five timed stages left to right: request intake with a half-hour budget, lot scoping with three hours, chain reconstruction with eight hours, formatting to the FDA template with two hours, and delivery with one hour. Cumulative elapsed time is shown under each stage, reaching fourteen and a half hours at delivery. A summary bar states that the total consumed is well inside the 24-hour deadline, leaving a nine-and-a-half-hour buffer for reruns and FDA clarifications. FSMA 204 24-hour response — per-stage timing budget Intake + auth budget 0.5h Scope lots budget 3h Reconstruct budget 8h Format export budget 2h Deliver budget 1h cum 0.5h cum 3.5h cum 11.5h cum 13.5h cum 14.5h Total consumed 14.5h — inside the 24-hour deadline 9.5h buffer held for reruns, slow queries, and FDA clarifications
Each stage is timed against its own slice of the 24-hour window; the retained buffer is the recovery margin for a real incident.

Output Columns and the FDA Sortable Spreadsheet

The deliverable is a sortable electronic spreadsheet whose columns FDA expects to see in a recognizable, machine-readable layout. The orchestrator’s format stage projects each reconstructed Critical Tracking Event into exactly these columns, drawing the canonical field definitions from the KDE Field Mapping Guide. The mapping below binds each internal KDE to its output column, its type, and the Subpart S provision that requires it. Optional columns are always emitted, filled with an explicit empty value rather than omitted, so the column count is stable across every response and the file remains sortable on any field.

Internal KDE (output column) FDA sortable-spreadsheet column Type Regulatory Source (21 CFR Part 1, Subpart S)
traceability_lot_code Traceability Lot Code str § 1.1320 (Traceability Lot Code assignment)
tlc_source_location Traceability Lot Code Source / Source reference str § 1.1320(a) (TLC source location)
product_description Traceability Product Description str § 1.1340(a) / § 1.1345(a) (product description KDE)
quantity Quantity Decimal § 1.1340(a) / § 1.1345(a) (quantity of food)
unit_of_measure Unit of Measure enum § 1.1340(a) / § 1.1345(a) (unit of measure)
location_description Location Description str § 1.1340 / § 1.1345 (ship-from / receive-to location)
event_type Event Type (CTE) enum § 1.1315 (Critical Tracking Event definitions)
event_date Event Date date § 1.1340 / § 1.1345 (date of the CTE)
reference_document_type Reference Document Type str § 1.1340(a) / § 1.1345(a) (reference document)
reference_document_number Reference Document Number str § 1.1340(a) / § 1.1345(a) (reference document)

The column order is contractual, not cosmetic: an investigator sorts and filters this file directly, so a stable header row and a deterministic row order are part of the compliance guarantee, not a nicety. The mechanics of writing that file — deterministic sort keys, UTF-8 encoding, and CSV or XLSX output that opens cleanly — are the subject of the dedicated sortable spreadsheet export guide, which this orchestrator calls in its format stage. Here the concern is only that every reconstructed event carries the full column set before it reaches that writer.

Production Orchestrator with Per-Stage Deadlines

The orchestrator is the piece that turns five capabilities into one deadline-bound response. It models the request and each stage result with pydantic v2, times every stage against its budget with asyncio.timeout, and aborts a stage the instant it overruns so the failure is loud and early rather than a silent deadline miss discovered at hour 23. The stage bodies below stand in for the real scoping and reconstruction calls; in production each delegates to the sibling guides’ implementations, but the timing, deadline enforcement, and structured observability are exactly as shown.

from __future__ import annotations

import asyncio
import logging
import time
from dataclasses import dataclass, field
from datetime import date, datetime, timezone
from decimal import Decimal

from pydantic import BaseModel, ConfigDict, Field, field_validator

logger = logging.getLogger("fsma204.response")
logger.setLevel(logging.INFO)

# Per-stage budgets in seconds, summing to 14.5h — well under the 24h SLA.
STAGE_BUDGETS: dict[str, float] = {
    "intake": 0.5 * 3600,
    "scope": 3 * 3600,
    "reconstruct": 8 * 3600,
    "format": 2 * 3600,
    "deliver": 1 * 3600,
}
SLA_DEADLINE_SECONDS: float = 24 * 3600


class StageOverrunError(RuntimeError):
    """Raised when a response stage exceeds its allotted time budget."""

    def __init__(self, stage: str, budget_s: float) -> None:
        super().__init__(f"stage '{stage}' overran its {budget_s / 3600:.2f}h budget")
        self.stage = stage
        self.budget_s = budget_s


class DeadlineExceededError(RuntimeError):
    """Raised when cumulative elapsed time would breach the 24-hour SLA."""


class FDARequest(BaseModel):
    """A parsed, authenticated FDA records request."""

    model_config = ConfigDict(strict=True)

    request_id: str = Field(..., min_length=3, max_length=64)
    product_description: str = Field(..., min_length=2)
    lot_codes: list[str] = Field(default_factory=list)
    window_start: date
    window_end: date
    received_at: datetime

    @field_validator("received_at")
    @classmethod
    def _tz_aware(cls, v: datetime) -> datetime:
        if v.tzinfo is None:
            raise ValueError("received_at must be timezone-aware")
        return v.astimezone(timezone.utc)


class TraceRow(BaseModel):
    """One reconstructed CTE, already shaped to the FDA output columns."""

    model_config = ConfigDict(strict=True)

    traceability_lot_code: str
    product_description: str
    quantity: Decimal = Field(..., gt=0)
    unit_of_measure: str
    location_description: str
    event_type: str
    event_date: date


@dataclass
class StageTiming:
    stage: str
    elapsed_s: float
    budget_s: float

    @property
    def within_budget(self) -> bool:
        return self.elapsed_s <= self.budget_s


@dataclass
class ResponseResult:
    request_id: str
    rows: list[TraceRow] = field(default_factory=list)
    timings: list[StageTiming] = field(default_factory=list)
    export_path: str | None = None

    @property
    def total_elapsed_s(self) -> float:
        return sum(t.elapsed_s for t in self.timings)


async def _run_stage(name: str, coro) -> tuple[object, StageTiming]:
    """Run one stage under its budget, timing it and enforcing the deadline."""
    budget = STAGE_BUDGETS[name]
    start = time.monotonic()
    try:
        async with asyncio.timeout(budget):
            value = await coro
    except TimeoutError as exc:
        elapsed = time.monotonic() - start
        logger.error(
            "STAGE_OVERRUN | stage=%s | elapsed_s=%.1f | budget_s=%.1f",
            name, elapsed, budget,
        )
        raise StageOverrunError(name, budget) from exc
    elapsed = time.monotonic() - start
    timing = StageTiming(stage=name, elapsed_s=elapsed, budget_s=budget)
    logger.info(
        "STAGE_OK | stage=%s | elapsed_s=%.1f | budget_s=%.1f | within=%s",
        name, elapsed, budget, timing.within_budget,
    )
    return value, timing


class ResponseOrchestrator:
    """Drives the five response stages within the 24-hour SLA."""

    def __init__(self, scoper, reconstructor, exporter) -> None:
        self._scope = scoper
        self._reconstruct = reconstructor
        self._export = exporter

    async def run(self, request: FDARequest) -> ResponseResult:
        result = ResponseResult(request_id=request.request_id)

        lots, t = await _run_stage("intake", self._intake(request))
        result.timings.append(t)
        self._check_deadline(result)

        scoped, t = await _run_stage("scope", self._scope(request, lots))
        result.timings.append(t)
        self._check_deadline(result)

        rows, t = await _run_stage("reconstruct", self._reconstruct(scoped))
        result.rows = rows
        result.timings.append(t)
        self._check_deadline(result)

        formatted, t = await _run_stage("format", self._format(rows))
        result.rows = formatted
        result.timings.append(t)
        self._check_deadline(result)

        path, t = await _run_stage("deliver", self._export(request, formatted))
        result.export_path = path
        result.timings.append(t)
        self._check_deadline(result)

        logger.info(
            "RESPONSE_COMPLETE | request_id=%s | rows=%d | total_h=%.2f",
            request.request_id, len(result.rows), result.total_elapsed_s / 3600,
        )
        return result

    async def _intake(self, request: FDARequest) -> list[str]:
        # Resolve the request to concrete seed lot codes.
        return list(request.lot_codes)

    async def _format(self, rows: list[TraceRow]) -> list[TraceRow]:
        # Rows already conform to the output-column contract; validate the set.
        return [TraceRow.model_validate(r.model_dump()) for r in rows]

    def _check_deadline(self, result: ResponseResult) -> None:
        if result.total_elapsed_s > SLA_DEADLINE_SECONDS:
            logger.critical(
                "SLA_BREACH | request_id=%s | total_h=%.2f",
                result.request_id, result.total_elapsed_s / 3600,
            )
            raise DeadlineExceededError(
                f"cumulative {result.total_elapsed_s / 3600:.2f}h exceeds 24h SLA"
            )

Two enforcement mechanisms work together. asyncio.timeout(budget) bounds each stage so a runaway reconstruction query cannot silently consume hours that belong to formatting and delivery; when it fires, _run_stage translates the raw TimeoutError into a StageOverrunError naming the offending stage. Independently, _check_deadline sums the real elapsed time after each stage and refuses to continue if the cumulative cost has breached the 24-hour SLA, so a series of within-budget-but-slow stages still trips a loud SLA_BREACH log rather than a missed handoff. Every stage emits a structured STAGE_OK line with its elapsed and budgeted seconds, which is exactly the telemetry a benchmark harness needs to build a per-stage latency distribution under load.

Error Handling When a Stage Overruns

An overrun is not merely a logged warning; it is an operational decision point, and the orchestrator is built to make that decision cheap. Because each stage is timed and bounded independently, a StageOverrunError tells the operator precisely which capability blew its budget and how much of the 24-hour window remains — the reconstruct stage overran, eleven hours are gone, and thirteen remain. That is enough context to choose between three responses: rerun the stage with a narrower scope, degrade to a partial-chain result annotated through the Fallback Routing Logic, or escalate to FDA for a reasonable extension. The code never has to guess, because the budget breach surfaces the instant it happens rather than at delivery time.

The distinction between a stage overrun and a hard SLA breach matters. StageOverrunError is recoverable: a single slow stage that trips its slice still leaves the retained buffer intact, so the orchestrator can be re-driven for that stage without abandoning the response. DeadlineExceededError is terminal for the automated path — it means the cumulative budget is genuinely exhausted, and the only correct action is to deliver whatever complete portion of the chain exists and document the gap for the investigator. In both cases the timing record on ResponseResult becomes part of the audit trail. Persisting that record, together with the request and the delivered file’s hash, into the audit log export is what lets a facility prove, after the fact, that it met its obligation and exactly how long each stage took.

Integration with the Recall Response Program

This orchestrator is the coordinating layer of the parent Recall Simulation & FDA 24-Hour Response reference, and it is intentionally thin: it owns timing, deadlines, and observability, and delegates every substantive query to the guides that specialize in it. The scope stage calls the lot-level recall scoping logic to expand a seed request into the full set of implicated Traceability Lot Codes, honoring the transformation rules that create new lot codes downstream. The reconstruct stage calls the one-up, one-back reconstruction logic to walk the trace graph and gather every relevant Critical Tracking Event. The format stage invokes the sortable spreadsheet export writer to project those events into the FDA column layout.

The evidence side of the response closes the loop with the compliance model. The delivered file, its SHA-256 hash, the parsed request, and the full per-stage timing record are written to the audit log export, which projects them into the FDA submission template and the facility’s own recordkeeping. Because the whole pipeline is built to be run on demand, it is also the exact code path exercised by the mock recall drills — a drill is simply a response run against a synthetic request, and the timing distribution those drills produce is the benchmark that proves the 24-hour guarantee is real rather than aspirational. Upstream, the quality of every row depends on the Critical Tracking Event records captured at ingestion, so a facility that neglects KDE completeness cannot reconstruct a defensible chain no matter how fast this orchestrator runs.

Operational Notes

Run the orchestrator as an on-demand job — a triggered worker or a container invoked by the incident tooling — not a long-lived service, so a crashed response is restarted cleanly and the request record remains the single source of truth. Recommended runtime and dependencies:

  • Python 3.11+ — asyncio.timeout is a 3.11 addition, and the code uses X | Y unions and modern generics.
  • pydantic ≥ 2.5 for the v2 field_validator / model_validate / model_dump API; never mix in the v1 validator decorator.
  • openpyxl ≥ 3.1 (or the standard-library csv module) in the export stage, covered in the dedicated export guide.

Configuration belongs in the environment. Provide the STAGE_BUDGETS as tunable values so a facility with a larger graph can reallocate the 24 hours without editing code, an AUDIT_LOG_SINK for the timing and delivery record, and the connection details for the trace store the scope and reconstruct stages query. Every response must log the request identifier, the per-stage elapsed and budgeted seconds, the total wall-clock cost, and the delivered file hash, because those are the fields an investigator or an internal auditor will ask for when confirming the response met its deadline. Rehearse the pipeline on a schedule; a benchmark that is only ever run during a real outbreak is not a benchmark. Retention of the response artifacts must satisfy the FSMA 204 two-year recordkeeping mandate described in the Data Retention Policies and detailed in the FDA FSMA 204 final rule.

Frequently Asked Questions

Does the 24-hour SLA start when the outbreak begins or when the request arrives?

It starts when FDA issues the records request. FSMA 204 requires the sortable electronic spreadsheet within 24 hours of that request, or within some reasonable time the agency agrees to. The orchestrator anchors its clock to the request’s timezone-aware received_at field, which is why intake parses and stores that timestamp before any query runs.

Why give reconstruction eight hours when the other stages are so small?

Chain reconstruction is the stage whose cost scales with graph size and query load, so it is the one most likely to slow down under real production volumes during an outbreak. Allocating it the largest slice keeps the deadline realistic, and timing it independently means a slow traversal is caught against its own budget rather than stealing time from formatting and delivery.

What happens if a single stage overruns its budget?

The stage raises a StageOverrunError naming exactly which capability blew its slice, and the orchestrator stops before starting the next stage. Because the total budget sums to only 14.5 hours, the retained buffer usually leaves room to rerun that stage with a narrower scope, degrade to a documented partial result, or request a reasonable extension from the investigator.

How is this different from just running the queries as fast as possible?

Raw speed does not prove compliance; a bounded, observable response does. By timing each stage against an explicit budget and enforcing a hard cumulative deadline in code, the orchestrator turns the 24-hour obligation into an enforced contract, and it produces a per-stage timing record that an auditor can inspect to confirm the facility met its deadline.

Where does the delivered spreadsheet and its timing record end up?

The delivered file, its SHA-256 hash, the parsed request, and the full per-stage timing record are written to the audit log export, which projects them into the FDA submission template and the facility’s own two-year recordkeeping store. That artifact is the evidence that the response happened, was on time, and matched what was handed to the investigator.

Which 21 CFR Part 1 sections define the columns in the deliverable?

Subpart S. The Critical Tracking Event definitions come from § 1.1315, Traceability Lot Code assignment and its source location from § 1.1320, and the per-event KDEs — product description, quantity, unit of measure, location, and reference document — from the shipping and receiving provisions in § 1.1340 and § 1.1345. The output-column table cites the source for each column.

Up: Recall Simulation & FDA 24-Hour Response — this orchestrator is the deadline-bound coordinating layer of the parent recall-response reference.