ARINC 633 is the standard that defines the Electronic Flight Folder — the package of documents a dispatcher sends to the cockpit before a flight. The standard is versioned: 633-1, 633-2, 633-3, 633-4, 633-5. Each revision expanded or clarified the previous one.

In theory, you implement the latest revision and you're done. In practice, what arrives from a real airline dispatcher is a different problem entirely.

What the standard defines and what airlines actually send

The ARINC 633 specification defines the structure of an EFF package: the file naming convention, the eff.xml manifest format, the folder tree, the document types, the pre-flight data fields. It is reasonably well-specified for a standards document.

What it does not control is the content inside the Operational Flight Plan — the OFP. The OFP is the central document in any EFF package. It contains the route, fuel figures, weights, ATC flight plan, weather summaries, alternates, and a dozen other items the crew needs before pushing back.

Airlines generate OFPs through their flight planning systems — Lido, Jeppesen, PPS, Leon, and others. Each of these systems has its own output format. Each airline configures that system differently. And airlines have been generating OFPs in their current format for years, sometimes decades. They do not change the format just because a new ARINC revision was published.

The result is that every airline sends you a structurally valid ARINC 633 package — correct eff.xml, correct file naming, correct folder tree — wrapped around an OFP that is entirely their own invention.

The versioning problem in practice

When you build a parser for ARINC 633 EFF packages, the version number in the package tells you relatively little about what the OFP inside looks like. Two airlines both sending ARINC 633-4 packages may have OFPs that share almost no structural similarity.

The fields you care about — block-off time, fuel on board, take-off weight, centre of gravity, alternate airports, flight level — are present in all OFPs, but they appear in different positions, under different labels, in different formats.

Some common variations I have encountered:

Why forking is the wrong answer

The obvious response to this variation is to write a separate parser per airline. You encounter a new airline, you write a parser for their OFP format, you maintain it separately. Many integrations work this way.

The problem is that OFP formats change. An airline upgrades their flight planning system. A new ARINC revision introduces a field that dispatchers start populating. The airline changes their fuel unit convention after a fleet change. Now you are maintaining N parsers, each of which can break independently, and you have no systematic way to detect when one has broken because it still parses without errors — it just silently returns wrong values.

I have seen this happen. A fuel figure parser that had been working for two years started returning values 2.2 times too high after an airline switched from kilograms to pounds. Nothing broke. The EFB happily displayed the wrong fuel figures.

A more robust approach

The approach that has worked better for me is to separate the three concerns that get conflated in a version-first model:

1. Structural validation — does the EFF package conform to the declared ARINC version? This is the only place where the version number matters. Validate the eff.xml, the file naming, the folder tree against the spec for that version. Reject packages that fail structural validation early.

2. Content identification — which airline and flight planning system generated this OFP? This is a detection problem, not a version problem. Build a registry keyed by (airline ICAO code, flight planning system identifier, format fingerprint). Most flight planning systems leave identifiable markers in the OFP header.

3. Field extraction — given the identified format, extract the values. Each entry in the registry maps to a set of extraction rules for that specific format. Rules can be shared across formats where the field layout is identical.

This separates the stable part (ARINC structural validation) from the variable part (OFP content extraction) and makes the variable part explicit rather than buried in version conditionals.

Detecting format breaks

The other thing worth building early is a validation step that runs after extraction, not before. Once you have extracted all fields, check that the values are internally consistent:

These checks catch the silent failures — the unit conversion that went wrong, the field that moved positions after a format update — before they reach the crew. They also give you a signal when an airline changes their OFP format: extraction starts succeeding structurally but consistency checks start failing.

It is not a substitute for testing against real packages from every airline you integrate with. But it is the difference between finding out a parser is broken in your test environment and finding out in the cockpit.

How the database architecture absorbs version changes

The parsing strategy above handles the incoming format. There is a second problem: what do you persist, and how do you evolve that schema when a new ARINC revision introduces fields you did not anticipate?

The naive approach couples two things that should be independent: the parser and the schema. Each new ARINC revision may introduce new pre-flight data elements — you update the parser to read the new fields, and you migrate the schema to store them. The two changes must be deployed together. If they get out of sync, either the parser writes to columns that do not exist yet, or the schema has columns the parser never fills. Every revision becomes a coordinated schema migration, and no migration helps the historical data you already ingested — that raw content is gone.

A more durable design decouples parser updates from schema changes entirely. The parser can be updated and re-run at any time. The schema only needs to change when you decide a new field belongs in the canonical table — which is your decision, not the standard's. Fields you query → canonical column. Fields you want available but do not query yet → they stay in topic_raw until you need them, with no schema change required.

A more durable design splits storage into two layers: a canonical record of what you have extracted, and a raw record of everything that arrived. Two tables carry this: flight_external_ref and topic_raw. These are not ARINC-defined terms — they are names I use for database tables that implement a particular ingestion pattern. Call them whatever fits your codebase; the structure is what matters.

The overall flow

  EFF package arrives
         │
         ▼
  ┌─────────────────────┐
  │ Structural          │  validate: eff.xml, file naming,
  │ Validation          │  folder tree vs. declared ARINC rev
  └─────────────────────┘
         │ pass
         ▼
  ┌─────────────────────┐
  │ flight_external_ref │  record: source system, external ID,
  │ (one row)           │  arinc_rev, received_at
  └─────────────────────┘
         │
         ├──────────────────────────────────┐
         ▼                                  ▼
  ┌─────────────────────┐     ┌─────────────────────────┐
  │ topic_raw           │     │ Parser                  │
  │ (one row per topic) │     │ identify format →       │
  │ raw content as-is   │     │ extract canonical fields │
  └─────────────────────┘     └─────────────────────────┘
                                             │
                                             ▼
                              ┌─────────────────────────┐
                              │ flights (canonical)     │
                              │ tow_kg, fob_kg,         │
                              │ cg_percent, blk_off_utc │
                              └─────────────────────────┘

Every ingested package produces three writes: one row in flight_external_ref, one or more rows in topic_raw, and an update to the canonical flights record. The raw content and the extracted values are stored independently. Neither depends on the other being correct.

flight_external_ref — linking to the source, not copying it

flight_external_ref links a canonical flight record to every external system that knows about that flight. One row per source system per flight.

CREATE TABLE flight_external_ref (
    id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    flight_id   uuid NOT NULL REFERENCES flights(id),
    source      text NOT NULL,   -- 'pps', 'lido', 'jeppesen', …
    external_id text NOT NULL,   -- the ID used by the source system
    arinc_rev   text,            -- '633-4', '633-5', … (null if unknown)
    received_at timestamptz NOT NULL DEFAULT now()
);

A real row for a PPS-sourced flight looks like:

id          | 3f2a1c…
flight_id   | 9b4e7d…   ← your internal canonical flight ID
source      | pps
external_id | DLH4221-20260628-EDDF-EGLL
arinc_rev   | 633-5
received_at | 2026-06-28 05:14:33+00

This does two things. It records which system sent the data and under which revision — information you will need later when re-processing. And it decouples your canonical flight identity from the upstream identifier. If the dispatcher sends the same flight twice under slightly different IDs — different date formats, corrected registration — you get two flight_external_ref rows pointing at one canonical flights row, rather than two conflicting records.

There is a third benefit that only becomes obvious later: flight_external_ref is a live audit of ARINC version adoption across all your connected systems. At any point you can query which vendors are on which revision, when a transition happened, and which sources have not yet moved to a newer revision:

-- Which ARINC versions is each source currently sending?
SELECT   source,
         arinc_rev,
         COUNT(*)        AS flight_count,
         MAX(received_at) AS last_seen
FROM     flight_external_ref
GROUP BY source, arinc_rev
ORDER BY source, arinc_rev;
source      | arinc_rev | flight_count | last_seen
------------+-----------+--------------+---------------------
jeppesen    | 633-4     |         1842 | 2026-01-14 22:11:00
jeppesen    | 633-5     |        38204 | 2026-06-28 05:14:33
lido        | 633-5     |        54901 | 2026-06-28 04:58:12
pps         | 633-4     |         9307 | 2026-06-28 05:01:44

From this result you can see immediately that Jeppesen completed a transition from 633-4 to 633-5 in January, Lido is already on 633-5, and PPS is still sending 633-4 packages. No additional instrumentation needed — the data was already being recorded as part of normal ingestion.

When a new revision is released, you can track adoption in real time. When a vendor claims they have upgraded, you can verify it. And when you need to plan a parser update, you know exactly which revision each source is on and how many historical flights will need re-processing.

topic_raw — store the source, parse later

topic_raw stores the unprocessed source content exactly as it arrived. One row per topic per flight per ingestion.

CREATE TABLE topic_raw (
    id           uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    flight_id    uuid NOT NULL REFERENCES flights(id),
    source_ref   uuid REFERENCES flight_external_ref(id),
    topic        text NOT NULL,   -- 'ofp', 'eff_manifest', 'pre_flight', …
    content      text NOT NULL,   -- raw bytes, exactly as received
    content_type text NOT NULL,   -- 'text/plain', 'application/xml', …
    received_at  timestamptz NOT NULL DEFAULT now()
);

Topics map to the distinct document types inside an EFF package. A typical ingestion for one flight produces:

topiccontent_typewhat it contains
eff_manifestapplication/xmlthe eff.xml folder tree
ofptext/plainthe full OFP text
pre_flightapplication/xmlARINC 633-5 pre-flight XML (if present)
atc_fptext/plainICAO 4444 ATC flight plan

The canonical flights table holds the extracted, normalised values — tow_kg, fob_kg, cg_percent, blk_off_utc — populated by the parser at ingestion time. topic_raw holds everything that came in, whether or not the parser could use it.

Re-processing when the format changes

This is where the architecture pays off. When a future revision introduces new fields, or when you improve a parser to extract fields it previously missed, you do not need to ask the dispatcher to resend anything. The raw content is already in topic_raw.

  topic_raw (historical)
         │
         │  SELECT content WHERE topic = 'pre_flight'
         │  AND arinc_rev IN ('633-4', '633-5')
         ▼
  ┌─────────────────────┐
  │ Updated parser      │  now understands new fields
  └─────────────────────┘
         │
         ▼
  ┌─────────────────────┐
  │ UPDATE flights      │  canonical values corrected
  │ SET new_field = …   │  for every historical flight
  └─────────────────────┘

In SQL:

-- Retrieve all stored pre-flight content for re-processing
SELECT tr.flight_id, tr.content
FROM   topic_raw tr
JOIN   flight_external_ref ref ON ref.id = tr.source_ref
WHERE  tr.topic    = 'pre_flight'
AND    ref.arinc_rev IN ('633-4', '633-5')
ORDER  BY tr.received_at;

Feed each row through the updated parser, then write the new extracted values back to flights. You can do this as a background migration with no downtime — the existing canonical values remain valid until the re-parse completes and overwrites them.

The same applies to airline-variant fixes. When you correct a unit conversion bug for a specific operator, you re-process only their rows by filtering on ref.source and ref.external_id patterns. The fix is retroactive.

The practical trade-off

Storing raw content costs storage. A typical EFF package is a few hundred kilobytes. Across tens of thousands of flights a year that is manageable; across millions it becomes a real cost. The trade-off is deliberate: raw storage buys retroactive correctability and forward compatibility with format changes you have not seen yet.

The alternative — parse at ingestion, discard the raw input — means every parsing mistake is permanent and every field a future revision introduces is unrecoverable for historical flights. For safety-relevant flight data, that is not a trade-off I am willing to make.

Knowing when parsing failed

The architecture above handles what happens when parsing succeeds. There is an equally important question: how do you find out when it did not?

There are two distinct failure modes. A hard failure is when the parser throws an exception, cannot identify the format, or produces no output at all. A soft failure is when extraction completes but the post-extraction consistency checks fail — values were extracted, but they do not add up correctly, suggesting a unit error, a field that moved, or a format change you have not seen yet.

Both failures are silent by default. Neither one crashes the application. Without explicit tracking, the only way you find out is when a crew reports wrong data in the cockpit — which is too late.

Tracking parse status in flight_external_ref

Add a parse_status column to flight_external_ref. This column records the outcome of the last parse attempt for each flight:

ALTER TABLE flight_external_ref
  ADD COLUMN parse_status  text NOT NULL DEFAULT 'pending',
  ADD COLUMN parse_error   text,
  ADD COLUMN parsed_at     timestamptz;

-- parse_status values: 'pending' | 'ok' | 'failed' | 'needs_review'

The parser writes to this column at the end of every ingestion:

parse_error stores a short human-readable message when the status is failed or needs_review: the check that failed, the values that were inconsistent, or the exception message. Without this, a failed row tells you something broke but not what.

Querying for failures

With this column in place, you can query for problems at any time:

-- All flights that need attention right now
SELECT   ref.source,
         ref.external_id,
         ref.arinc_rev,
         ref.parse_status,
         ref.parse_error,
         ref.received_at
FROM     flight_external_ref ref
WHERE    ref.parse_status IN ('failed', 'needs_review')
ORDER BY ref.received_at DESC;
source    | external_id              | arinc_rev | parse_status | parse_error
----------+--------------------------+-----------+--------------+-----------------------------
pps       | DLH4221-20260628-EDDF-EGLL | 633-4   | needs_review | TOW/ZFW delta 4.1T > 0.5T threshold — possible unit mismatch
jeppesen  | AAL1138-20260627-KJFK-KLAX | 633-5   | failed       | format fingerprint not matched — unknown OFP header

The first row tells you the unit check failed for a specific PPS flight. The second row tells you Jeppesen sent a format the parser does not recognise — probably a new OFP template.

You can also query how many flights are waiting:

SELECT   parse_status, COUNT(*) AS count
FROM     flight_external_ref
GROUP BY parse_status;
parse_status  | count
--------------+-------
ok            | 92184
pending       |     3
needs_review  |    14
failed        |     6

This kind of summary is what you put on an operations dashboard or feed into a scheduled alert.

Getting notified

The simplest approach is a background job that runs this query on a schedule — every few minutes is enough — and sends a notification when failed or needs_review count exceeds a threshold:

every 5 minutes:
  SELECT COUNT(*) FROM flight_external_ref
  WHERE parse_status IN ('failed', 'needs_review')
  AND received_at > now() - interval '1 hour'

  if count > 0:
    send alert: "N flight packages failed parsing in the last hour"

The alert tells you there is a problem. The detail query tells you which flights and why. Because topic_raw still holds the raw content, the fix cycle is: investigate the failed row, update the parser, re-run it against the stored raw content, clear the parse_status back to ok.

The pending status is also worth monitoring. A flight that stays in pending for more than a few minutes means the parser never ran — a crash, a queue backup, or a deployment issue. It is a different class of problem from a parse failure, but just as invisible without explicit tracking.

← Back