Parsing ACARS OOOI Messages into Duty Events

The exact task this guide solves is turning the stream of ACARS OOOI movement messages — Out of the gate, Off the ground, On the ground, In at the gate — into the validated, UTC-normalised duty events a compliance engine reasons about, and deriving block and flight time from them. OOOI reports are the ground truth for when an aircraft actually moved, which makes them the authoritative source for block-to-block and airborne durations, but they arrive as terse, out-of-order text that must be parsed, ordered, and validated before any duration is trustworthy. This page parses the four events, reconstructs the flight leg, and derives the times. It is part of the flight log parsing pipelines section and produces the segment boundaries the duty time validation rule engines consume.

Prerequisites

The four events and what they bound

An OOOI sequence records four instants for a leg. Out is push-back from the gate, which starts block time. Off is wheels-up, which starts flight time. On is wheels-down, which ends flight time. In is arrival at the gate, which ends block time. Two durations fall straight out of them: block time is In minus Out, and flight (airborne) time is On minus Off. The parser’s job is to group the four messages into a leg, confirm they are complete and ordered, and derive the two durations — because a missing or out-of-order event silently corrupts every downstream calculation that trusts the block boundary.

Step 1 — Parse a raw message into a typed event

Each message is parsed into a typed event with a validated timestamp, rejecting anything without an offset at the boundary. An enum fixes the four legal event types so a typo cannot introduce a fifth.

from dataclasses import dataclass
from datetime import datetime, timezone
from enum import Enum


class OooiKind(Enum):
    OUT = "OUT"
    OFF = "OFF"
    ON = "ON"
    IN = "IN"


@dataclass(frozen=True, slots=True)
class OooiEvent:
    flight_id: str
    kind: OooiKind
    instant_utc: datetime


def parse_message(flight_id: str, kind: str, raw_ts: str) -> OooiEvent:
    parsed = datetime.fromisoformat(raw_ts)
    if parsed.tzinfo is None:
        raise ValueError(f"naive OOOI timestamp rejected: {raw_ts!r}")
    return OooiEvent(flight_id, OooiKind(kind), parsed.astimezone(timezone.utc))

Verify: parse_message("UA123", "OFF", "2026-07-17T14:05:00-04:00") yields an OFF event at 18:05Z; a message with kind "PUSH" raises a ValueError from the enum, and a naive timestamp raises at the offset check.

Step 2 — Assemble a complete, ordered leg

The four events for a leg must all be present and satisfy Out ≤ Off ≤ On ≤ In. Grouping by flight id and validating the ordering catches the missing and misordered events that are the common corruption in an OOOI feed.

def assemble_leg(events: list[OooiEvent]) -> dict:
    by_kind = {e.kind: e for e in events}
    missing = [k.value for k in OooiKind if k not in by_kind]
    if missing:
        raise ValueError(f"incomplete OOOI leg, missing {missing}")
    out, off, on, inn = (by_kind[k].instant_utc for k in
                         (OooiKind.OUT, OooiKind.OFF, OooiKind.ON, OooiKind.IN))
    if not out <= off <= on <= inn:
        raise ValueError("OOOI events out of order: expected OUT ≤ OFF ≤ ON ≤ IN")
    return {"out": out, "off": off, "on": on, "in": inn}

Verify: four in-order events assemble into a leg dict; a leg missing its IN message raises with missing=['IN']; a leg whose ON precedes its OFF raises the ordering error rather than producing a negative flight time.

Step 3 — Derive block and flight time

With an ordered leg, the two durations are simple subtractions of UTC instants — correct across any DST or date-line boundary because the instants are absolute.

def leg_durations(leg: dict) -> dict:
    block_minutes = (leg["in"] - leg["out"]).total_seconds() / 60.0
    flight_minutes = (leg["on"] - leg["off"]).total_seconds() / 60.0
    return {
        "block_minutes": round(block_minutes, 1),
        "flight_minutes": round(flight_minutes, 1),
        "taxi_minutes": round(block_minutes - flight_minutes, 1),
    }

Verify: a leg with Out 18:00Z, Off 18:20Z, On 20:50Z, In 21:00Z yields 180 block minutes, 150 flight minutes, and 30 taxi minutes — and the taxi figure equals block minus flight, a useful cross-check that the four events are internally consistent.

OOOI events bounding block and flight time A timeline shows four OOOI events in order: Out at push-back, Off at wheels-up, On at wheels-down, and In at the gate. Block time spans Out to In; flight time spans Off to On; the difference between them is taxi time at each end. OUT OFF ON IN 18:00Z 18:20Z 20:50Z 21:00Z block time — 180 min flight time — 150 min
Block time spans Out to In and flight time spans Off to On; the two derive directly from the four OOOI instants, and their difference is the taxi time that cross-checks the leg.

Failure modes and troubleshooting

Frequently Asked Questions

Why are OOOI events the authoritative source for block and flight time?

Because they record when the aircraft physically moved — push-back, wheels-up, wheels-down, gate arrival — rather than a scheduled or estimated time. Block and flight time derived from actual OOOI instants are what a regulator and the duty-time engine treat as ground truth.

What is the difference between block time and flight time?

Block time runs gate to gate — Out to In — and includes taxi at both ends. Flight time runs wheels-up to wheels-down — Off to On — and is the airborne portion only. Their difference is total taxi time, which is a useful consistency check on the leg.

How are out-of-order or missing events handled?

A leg must have all four events in the order Out ≤ Off ≤ On ≤ In. A missing event or a violated ordering means the leg cannot be trusted, so it is quarantined for review rather than producing a corrupt duration.

Why deduplicate OOOI messages?

Because ACARS may re-transmit the same event, and two OFF messages for one leg would otherwise overwrite or duplicate the airborne boundary. Deduplicating by flight id and event kind keeps one authoritative instant per event.

Back to Flight Log Parsing Pipelines.