A rotable component lives a long, branching life — received on a Form 8130-3, installed, flown, removed for a defect, repaired, reinstalled, and eventually scrapped — and every one of those moments is a record some downstream system needs to trust. Modeling that life as a single canonical event schema, rather than a scatter of table-specific rows, gives traceability, reliability, and compliance engines one shared contract to read. This page designs that event model as part of MRO Data Schema Design, grounded in the continuing-airworthiness record obligations of EASA Part-M M.A.305 and the release-documentation rules around FAA Form 8130-3.
Prerequisites & inputs
The canonical event model assumes each component is already identified by a stable key: a part number and serial number pair that uniquely names the physical unit — say part AV3025-19, serial SN-441027, tracked on aircraft N219FE. Events reference that key rather than duplicating descriptive attributes, so a name change on the master record never orphans history. Each event also carries a monotonic sequence anchor (an ISO 8601 UTC timestamp plus an ordering counter) so that two events on the same day still have an unambiguous order.
Upstream, the raw material for these events arrives in wildly different shapes from different vendors, which is why event construction depends on Data Normalization Across OEM Formats to reconcile part-number conventions and units before an event is minted. Downstream, the finished events are the primary feed into Parts-Traceability Ledger Design for Aviation Components, which chains them into an append-only provenance record.
The lifecycle as events
Step-by-step implementation
- Model events as a discriminated union. Every event shares a common envelope — component key, timestamp, actor, ATA reference — and a
event_typediscriminator selects the type-specific payload. Receipt needs acertificate_ref; install needs apositionand installing aircraft; scrap needs amutilation_method. A tagged union keeps each variant strictly typed without a sprawling nullable mega-table. - Make the envelope regulator-shaped. The shared fields are chosen so that any event can substantiate a continuing-airworthiness record: who performed the action, when in UTC, against which serialized unit, and under what release document.
- Enforce legal state transitions. A component cannot be installed before it is received, nor removed before it is installed. Encode the allowed transitions and reject an event that violates them at construction time rather than discovering the impossibility during an audit.
- Keep events immutable. A correction is a new compensating event, never an edit. This preserves the exact truth of what was recorded when, which is the whole point of an airworthiness trail.
- Emit a stable hash per event. Each finished event carries a SHA-256 over its canonical serialization so the downstream ledger can chain it and detect any later tampering.
The following module defines the canonical event union with Pydantic v2 and validates the transition against the unit’s prior state.
from __future__ import annotations
import hashlib
import json
import logging
from datetime import datetime, timezone
from enum import Enum
from typing import Annotated, Literal, Optional, Union
from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, field_validator
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
logger = logging.getLogger("mro.schema.lifecycle")
class LifecycleState(str, Enum):
IN_STOCK = "IN_STOCK"
INSTALLED = "INSTALLED"
IN_REPAIR = "IN_REPAIR"
SCRAPPED = "SCRAPPED"
class _Envelope(BaseModel):
"""Fields common to every lifecycle event.
Substantiates the continuing-airworthiness record required by
EASA Part-M M.A.305 (aircraft continuing-airworthiness record).
"""
model_config = ConfigDict(strict=True, extra="forbid")
part_number: str = Field(pattern=r"^[A-Z0-9]{2,}[A-Z0-9\-]{2,20}$")
serial_number: str = Field(min_length=1, max_length=32)
ata_chapter: str = Field(pattern=r"^\d{2}(-\d{2})?$")
actor_id: str = Field(min_length=3)
occurred_at: datetime
@field_validator("occurred_at", mode="before")
@classmethod
def to_utc(cls, v: object) -> datetime:
if isinstance(v, str):
return datetime.fromisoformat(v.replace("Z", "+00:00")).astimezone(timezone.utc)
if isinstance(v, datetime):
return v.astimezone(timezone.utc)
raise ValueError("occurred_at must be ISO 8601")
class ReceiptEvent(_Envelope):
event_type: Literal["RECEIPT"] = "RECEIPT"
# A serviceable rotable enters stock under an FAA Form 8130-3 release.
certificate_ref: str = Field(min_length=3)
class InstallEvent(_Envelope):
event_type: Literal["INSTALL"] = "INSTALL"
registration: str = Field(pattern=r"^[A-Z0-9\-]{2,10}$")
position: str = Field(min_length=1)
class RemovalEvent(_Envelope):
event_type: Literal["REMOVAL"] = "REMOVAL"
reason_code: str = Field(min_length=2)
sends_to_repair: bool = False
class ScrapEvent(_Envelope):
event_type: Literal["SCRAP"] = "SCRAP"
mutilation_method: str = Field(min_length=3)
LifecycleEvent = Annotated[
Union[ReceiptEvent, InstallEvent, RemovalEvent, ScrapEvent],
Field(discriminator="event_type"),
]
_ADAPTER: TypeAdapter[LifecycleEvent] = TypeAdapter(LifecycleEvent)
_ALLOWED = {
"RECEIPT": {None},
"INSTALL": {LifecycleState.IN_STOCK},
"REMOVAL": {LifecycleState.INSTALLED},
"SCRAP": {LifecycleState.IN_STOCK, LifecycleState.IN_REPAIR},
}
def parse_event(raw: dict, prior_state: Optional[LifecycleState]) -> tuple[BaseModel, str]:
"""Validate an event and confirm it is a legal transition from prior_state."""
event = _ADAPTER.validate_python(raw)
allowed = _ALLOWED[event.event_type]
if prior_state not in allowed:
raise ValueError(
f"{event.event_type} illegal from state {prior_state} for {event.serial_number}"
)
canonical = event.model_dump_json()
digest = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
logger.info(
"EVENT %s sn=%s ata=%s sha=%s",
event.event_type, event.serial_number, event.ata_chapter, digest[:12],
)
return event, digest
Worked example
A hydraulic accumulator, part AV3025-19 serial SN-441027, arrives with an 8130-3. The receipt event {"event_type": "RECEIPT", "part_number": "AV3025-19", "serial_number": "SN-441027", "ata_chapter": "29-31", "actor_id": "insp-7742", "occurred_at": "2026-07-10T09:15:00Z", "certificate_ref": "8130-3-A5591"} is parsed from prior_state = None, which is legal, and yields state IN_STOCK with hash 4c8be1f0a2d7....
Two days later an install event places it on N219FE at position LH-MLG-BAY; parsed from IN_STOCK it is legal and the unit moves to INSTALLED. Now suppose a bad feed replays the receipt event while the unit is INSTALLED. parse_event raises RECEIPT illegal from state INSTALLED for SN-441027 — the transition guard catches the impossible history before it can corrupt the ledger. Only a valid REMOVAL from the INSTALLED state is accepted, correctly recording the accumulator’s removal and, because sends_to_repair is true, routing it toward IN_REPAIR.
Edge cases & compliance gotchas
- Ambiguous ATA suffixes. A source that writes
29and another that writes29-31describe the same system at different depths. Normalize the ATA reference before the event is minted so reliability rollups by chapter do not split one system across two buckets. - Unicode in actor names. A certifying technician recorded as
Müllerin one feed andMullerin another must resolve to oneactor_id; key events on a stable identifier, not the display name, or the audit trail will appear to show two different signatories. - Timezone drift on same-day events. An install logged in local time and a removal logged in UTC can appear out of order and trip the transition guard falsely. Coerce every
occurred_atto ISO 8601 UTC at the boundary, exactly as the validator above does, before any ordering decision. - Scrap must be terminal. Once a
SCRAPevent with a recordedmutilation_methodlands, no later install may reference that serial — a scrapped unit reappearing on an aircraft is a bogus-parts red flag. The state machine makesSCRAPPEDabsorbing, so any subsequent transition is rejected outright.
A canonical lifecycle event schema turns a component’s scattered paperwork into a single, ordered, tamper-evident narrative. By fixing one envelope, a strict tagged union of event types, and a state machine that refuses impossible histories, the model gives every downstream traceability and compliance system a shared truth to build on — and makes the question “where has this serial been?” answerable in one query rather than a forensic reconstruction.