Crew Duty Time Taxonomy Mapping
Crew duty time taxonomy mapping is the translation layer that turns raw operational telemetry into deterministic, auditable compliance logic. In production flight operations, duty periods, rest windows, and standby classifications are rarely uniform across scheduling platforms, crew management systems, and jurisdictional mandates — a “standby” in one vendor’s roster feed may be an active duty block in another, and a split-duty rest may look identical to an overnight layover unless the boundaries are modelled explicitly. A rigorous taxonomy resolves this fragmentation by standardizing how duty states are defined, calculated, and audited. This mapping discipline is a foundational subsystem within the broader Core Architecture & Regulatory Mapping domain, ensuring every pairing adjustment, reserve callout, and fatigue-risk assessment resolves against the correct regulatory schema without manual reconciliation or brittle legacy workarounds.
Figure: Duty-state machine: explicit transitions prevent misclassifying standby, ground duty, or rest, each of which triggers a different regulatory calculation.
The classification gap this layer closes
The specific engineering challenge is that regulations reason about named states — flight duty period, rest period, standby, split duty — while operational systems emit events: an ACARS OUT/OFF/ON/IN quartet, a crew-app “report” tap, a rostering row with a shift code. Nothing in that raw stream declares which regulatory state applies. The gap between an event stream and a legally meaningful state is exactly where compliance errors originate: an airport standby that converts to a flight duty period must inherit its start time from the standby report, not from pushback, or the flight duty period (FDP) will be understated and a cumulative-limit breach will pass silently.
A taxonomy closes this gap by defining a small, closed set of canonical states, the boundary triggers that open and close each one, and the modifier flags that alter the downstream calculation (night duty, acclimatization status, augmented crew, time-zone crossing). Once every event resolves to exactly one canonical state with unambiguous start and end instants, the rule engine becomes a pure function of that normalized history. This is why the taxonomy is built and tested before any limit arithmetic: it is the contract that the FAA Part 117 rule schema design and the EASA FTL compliance frameworks both consume, and both jurisdictions must see identical temporal boundaries for a given roster.
Duty-state schema design
The schema centres on a normalized enumeration rather than free-text labels or proprietary vendor codes. Primary canonical states are Flight Duty Period, Ground Duty, Airport Standby, Hotel/Home Standby, Split Duty, Augmented Operations, and Rest Period. Each state carries explicit boundary conditions — a start trigger (report time, pushback, callout), an end trigger (chocks on, release, stand-down) — and a set of modifier flags. The persisted model separates three concerns: the immutable duty event (what the feed reported), the derived duty segment (a classified, bounded interval), and the aggregate duty period (a chain of contiguous segments the regulation treats as one unit).
The entity relationships matter because regulatory limits are computed at different levels. A single sector is a segment; §117.13 FDP limits apply to the period that groups those segments; and cumulative caps aggregate periods over rolling windows. Modelling these as one flat table forces every query to re-derive grouping logic, which is where subtle off-by-one boundary bugs live. Keeping raw events immutable also preserves audit lineage: when a compliance officer asks why a pairing was flagged, the system can replay the exact event stream, the classification decision, and the applied threshold.
Canonical field naming is standardized across the schema so that downstream modules — scheduling, payroll, compliance reporting — read identical semantics. Every temporal field is stored as timezone-aware UTC (start_utc, end_utc) with a separate report_tz IANA identifier retained for local-time-dependent rules such as the window-of-circadian-low and reporting-time-keyed FDP table lookups.
Regulatory mapping
The taxonomy exists to make specific regulatory sections computable, so each canonical state maps to the text that governs it.
- Rest Period anchors to 14 CFR §117.3 (definition) and §117.25, which requires a minimum of 10 consecutive hours of rest providing an opportunity for at least 8 uninterrupted hours of sleep before an FDP. The taxonomy must distinguish this regulatory rest from a discretionary layover, because only the former resets the applicable FDP table row.
- Flight Duty Period maps to §117.3 and the FDP limit tables in §117.13, which are keyed on scheduled report time (in the crew member’s acclimatised local time) and the number of flight segments. The taxonomy’s
report_tzandsegment_countfields exist precisely to drive this lookup. - Split Duty maps to §117.15: a rest opportunity provided within an FDP under specified conditions can extend the maximum FDP. The taxonomy models the in-duty rest as a distinct segment so the extension is calculated from real boundaries rather than assumed.
- Cumulative limits map to §117.23, which caps FDP at 60 hours in any 168 consecutive hours and 190 hours in any 672 consecutive hours, and flight time at 100 hours in any 672 hours. These are the rolling-window aggregations covered below.
For operators under European jurisdiction, the same canonical states re-map onto Regulation (EU) No 965/2012, Subpart FTL: ORO.FTL.105 (definitions), ORO.FTL.205 (maximum daily FDP with acclimatisation-dependent tables), ORO.FTL.235 (rest periods), and the cumulative duty limits in ORO.FTL.210. The critical design decision is that acclimatisation state — “acclimatised”, “unknown”, or a specific reference time — is a modifier flag on the state, not a separate code path, so the mapping layer can route the identical duty history to either the FAA or EASA schema without duplicating the state machine.
Python implementation walkthrough
Duty states are modelled as a StrEnum to guarantee immutability, type safety, and human-readable serialization at API boundaries:
from enum import StrEnum
class DutyState(StrEnum):
REST = "rest"
AIRPORT_STANDBY = "airport_standby"
HOTEL_STANDBY = "hotel_standby"
FLIGHT_DUTY = "flight_duty"
GROUND_DUTY = "ground_duty"
SPLIT_DUTY = "split_duty"
AUGMENTED = "augmented"
Ingested telemetry is validated at the boundary with a Pydantic model before it can reach the classifier. Naive timestamps are rejected outright, because a missing offset is the single most common source of cross-jurisdictional miscalculation:
from datetime import datetime
from zoneinfo import ZoneInfo
from pydantic import BaseModel, field_validator
class DutyEvent(BaseModel):
crew_id: str
state: DutyState
start_utc: datetime
end_utc: datetime
report_tz: str # IANA identifier, e.g. "Europe/Berlin"
segment_count: int = 0
is_night: bool = False
acclimatised: bool = True
@field_validator("start_utc", "end_utc")
@classmethod
def require_aware_utc(cls, v: datetime) -> datetime:
if v.tzinfo is None or v.utcoffset() is None:
raise ValueError("timestamps must be timezone-aware")
return v.astimezone(ZoneInfo("UTC"))
@field_validator("report_tz")
@classmethod
def known_zone(cls, v: str) -> str:
ZoneInfo(v) # raises if the IANA key is unknown
return v
Transitions between states are governed by a finite state machine that validates preconditions before allowing progression. This is what prevents a HOTEL_STANDBY from being silently promoted to FLIGHT_DUTY without first passing through a callout, and it is what carries the standby report time forward so the resulting FDP starts at the right instant:
ALLOWED: dict[DutyState, set[DutyState]] = {
DutyState.REST: {DutyState.AIRPORT_STANDBY, DutyState.HOTEL_STANDBY,
DutyState.FLIGHT_DUTY},
DutyState.AIRPORT_STANDBY: {DutyState.FLIGHT_DUTY, DutyState.REST},
DutyState.HOTEL_STANDBY: {DutyState.AIRPORT_STANDBY,
DutyState.FLIGHT_DUTY, DutyState.REST},
DutyState.FLIGHT_DUTY: {DutyState.GROUND_DUTY, DutyState.SPLIT_DUTY,
DutyState.REST},
DutyState.GROUND_DUTY: {DutyState.FLIGHT_DUTY, DutyState.REST},
DutyState.SPLIT_DUTY: {DutyState.FLIGHT_DUTY},
DutyState.AUGMENTED: {DutyState.REST},
}
def apply_transition(current: DutyState, nxt: DutyState) -> DutyState:
if nxt not in ALLOWED[current]:
raise ValueError(f"illegal transition {current} -> {nxt}")
return nxt
The classifier that assigns a canonical state to a raw event is kept as a pure function — it takes normalized input and returns a DutyState with no side effects — so it can be exhaustively unit-tested and re-run deterministically during audit replay.
Rolling-window and temporal aggregation patterns
The cumulative caps in §117.23 (and their EASA equivalents in ORO.FTL.210) are inherently windowed: no pairing is compliant or non-compliant in isolation. Once duty periods are normalized to UTC, the 168-hour and 672-hour look-backs are natural fits for SQL window functions over a RANGE frame:
SELECT
crew_id,
period_start_utc,
SUM(fdp_hours) OVER (
PARTITION BY crew_id
ORDER BY period_start_utc
RANGE BETWEEN INTERVAL '168 hours' PRECEDING AND CURRENT ROW
) AS fdp_168h,
SUM(fdp_hours) OVER (
PARTITION BY crew_id
ORDER BY period_start_utc
RANGE BETWEEN INTERVAL '672 hours' PRECEDING AND CURRENT ROW
) AS fdp_672h
FROM duty_period
ORDER BY crew_id, period_start_utc;
The same aggregation expressed with Polars keeps the calculation inside the Python pipeline for pre-publish validation, using a time-based rolling window that respects the UTC ordering:
import polars as pl
rolling = (
duty_periods
.sort("period_start_utc")
.rolling(index_column="period_start_utc", period="168h", group_by="crew_id")
.agg(pl.col("fdp_hours").sum().alias("fdp_168h"))
)
breaches = rolling.filter(pl.col("fdp_168h") > 60.0)
Two subtleties are specific to duty-time aggregation. First, the window is defined on the period start, but the cumulative cap is on hours accrued within the window, so periods that straddle the window boundary must be counted, not truncated — a RANGE frame keyed on the start instant handles this cleanly where a naive ROWS frame would not. Second, all arithmetic stays in UTC; local-time-dependent lookups (the §117.13 report-time row) are computed separately from the report_tz field and never mixed into the cumulative sum.
Integration points
The taxonomy sits between upstream feeds and downstream evaluators, so its contracts define how the surrounding subsystems interoperate:
- Upstream: normalized events arrive from the flight data ingestion and system sync pipeline. That layer owns raw ACARS/roster capture and UTC stamping; the taxonomy owns classification. Keeping the boundary sharp means an ingestion schema change cannot silently alter a regulatory classification.
- Downstream rule evaluation: classified duty periods feed the duty time validation and rule engines layer, which applies the FAA and EASA limit arithmetic. The taxonomy exposes a single state machine that routes each evaluation request to the correct regulatory schema.
- Real-time exposure: state transitions are published as immutable events rather than polled from static tables, and downstream applications read them through structured endpoints described in Building a REST API for Crew Rest Requirements. This guarantees rostering tools, mobile crew apps, and dispatch consoles query one source of truth and never drift out of version.
- Security and audit: ingestion and evaluation endpoints enforce the system security and access boundaries model — role-based and attribute-based access control, cryptographically signed transition logs for auditors, and write access to override flags only through documented exception workflows. When real-time validation is unavailable during a network partition, the taxonomy defaults to pre-calculated, jurisdictionally conservative limits so scheduling degrades gracefully rather than halting.
Testing and edge cases
Because the taxonomy is a pure classification function over timestamps, it is an ideal target for property-based testing with hypothesis. The properties worth asserting are invariants of the state machine rather than specific outputs:
from hypothesis import given, strategies as st
@given(
seq=st.lists(st.sampled_from(list(DutyState)), min_size=1, max_size=20)
)
def test_no_illegal_transitions_survive(seq):
state = DutyState.REST
for nxt in seq:
if nxt in ALLOWED[state]:
state = apply_transition(state, nxt)
else:
# illegal transitions must always raise, never coerce
try:
apply_transition(state, nxt)
except ValueError:
pass
else:
raise AssertionError("illegal transition was accepted")
The boundary conditions unique to this domain deserve dedicated regression cases: a duty period that crosses UTC midnight; a report time that falls on a daylight-saving transition in report_tz (a “missing” or “repeated” local hour); an airport standby that converts to flight duty exactly at the standby limit; a split-duty rest that is one minute short of the §117.15 qualifying threshold; and an acclimatisation state that flips between two sectors of the same period. Each of these has produced silent miscalculations in naive implementations, so each should assert against hand-verified expected boundaries and be replayed against historical pairing data before any deploy.
Continue in this area
- Building a REST API for Crew Rest Requirements — exposes the normalized duty states through a deterministic, timezone-aware validation endpoint that scheduling engines call before publishing a pairing.
A rigorously engineered crew duty time taxonomy turns fragmented operational data into a predictable, auditable compliance asset. By standardizing state definitions, routing evaluations through jurisdiction-specific schemas, and enforcing strict security and fallback protocols, flight operations teams scale scheduling complexity without compromising regulatory adherence. For authoritative references, the official Python zoneinfo documentation covers IANA time-zone handling, and the full text of 14 CFR Part 117 governs the cumulative limits and rest requirements the taxonomy is built to enforce.
Related
- FAA Part 117 Rule Schema Design — the sibling schema that consumes these canonical states for FAA limit arithmetic.
- EASA FTL Compliance Frameworks — the European rule set the same taxonomy re-maps onto via acclimatisation modifiers.
- System Security & Access Boundaries — access control and signed audit trails for taxonomy endpoints.
- Flight Data Ingestion & System Sync — the upstream pipeline that delivers UTC-stamped events for classification.
- Duty Time Validation & Rule Engines — the downstream evaluators that apply cumulative and FDP limits.
Back to Core Architecture & Regulatory Mapping.