Introduction
Contrail is a proposal to provide lexicons to describe flight segments. A contrail record
merely asserts that a particular flight existed, additional optional fields exist to
provide context. For instance, a user's connection to a particular flight (i.e. as a
passenger or pilot) is entirely optional. In fact, the only mandatory field required is
createdAt. This is the most important property of the schema and everything
else follows from it.
An absent field means the value is unknown or does not apply. It never means zero, empty, or none, and the schema defines no placeholder values. A record describing a flight where only the date and the tail number are known is as valid as one with every field populated. Tools that require complete data should enforce that requirement themselves, since the schema cannot do so without excluding general aviation, where most of the commercial fields have no meaning.
The relationship field is the one exception to this rule. When it is absent,
no claim is being made about the author's involvement, rather than the claim being
unknown. A record created from ADS-B data about another operator's aircraft simply omits
it.
Records for every kind of flying
Contrail is intended to describe any kind of flying. Scheduled airline service and general aviation are the most familiar examples, and they sit at roughly opposite ends of how much structured information a flight carries, but they are not the limits of the schema. Cargo, charter, business aviation, air ambulance, law enforcement, government, military and flight training operations all use the same record type.
The difference between them is only which fields happen to apply. A cargo flight has an operator and a flight number but no seat or cabin. A police helicopter may have nothing beyond a registration, an aircraft type, and a takeoff and landing time. Both are complete records, and so are the two below.
{
"$type": "com.airplaneian.contrail.temp.flight",
"createdAt": "2026-07-25T09:14:00-07:00",
"date": "2025-07-13",
"origin": { "iata": "SFO", "terminal": "INTL", "gate": "A2" },
"destination": { "iata": "LHR", "terminal": "3" },
"operator": "VIR",
"flightNumber": "20",
"marketingAirline": "DAL",
"marketingFlightNumber": "5964",
"registration": "GVBOW",
"aircraftType": "Boeing 787-9",
"actualGateDeparture": "2025-07-13T15:57:00-07:00",
"actualTakeoff": "2025-07-13T16:25:00-07:00",
"actualLanding": "2025-07-14T10:14:00+01:00",
"actualGateArrival": "2025-07-14T10:23:00+01:00",
"relationship": "passenger",
"seat": "46K",
"status": "normal",
"source": "flighty",
"sourceId": "bef37d51-2876-433b-bffd-95c81f8fe569"
}
{
"$type": "com.airplaneian.contrail.temp.flight",
"createdAt": "2026-07-25T09:14:00-07:00",
"date": "2026-07-19",
"origin": {
"name": "Twin Oaks Airpark",
"faaLid": "7S3",
"geo": { "latitude": "45.42361", "longitude": "-122.94028" }
},
"destination": {
"name": "Twin Oaks Airpark",
"faaLid": "7S3",
"geo": { "latitude": "45.42361", "longitude": "-122.94028" }
},
"registration": "N12345",
"icao24": "a1b2c3",
"icaoTypeDesignator": "PA18",
"aircraftType": "Piper PA-18 Super Cub",
"actualTakeoff": "2026-07-19T10:12:00-07:00",
"actualLanding": "2026-07-19T10:58:00-07:00",
"relationship": "pic",
"source": "manual"
}
Note that the origin and destination in the general aviation example are the same place. Local flights are ordinary and the schema treats them as such.
Identifying flights across repositories
Because a record describes the flight rather than the author, several people aboard the same aircraft may each write a record for it. None of these is canonical, and Contrail treats that duplication as expected. It does not attempt to define a global flight identifier, because no authority exists to issue one.
This does mean readers need some way to recognise two repositories describing the same flight, so the identifying fields carry more weight here than they would in a personal logbook. Write them consistently wherever they are known.
| Commercial | date + operator + flightNumber |
|---|---|
| General aviation | icao24 + actualTakeoff |
The operator field takes the ICAO designator of whoever actually flew the
aircraft, and is not restricted to airlines: UAL for an airline,
FDX for a cargo carrier, RCH for a military air mobility
flight. It is the ICAO designator rather than the IATA code (UA) because
IATA codes are reassigned between carriers over time, which makes a historical record
tagged with one ambiguous. Operators with no designator at all, which is most of general
aviation, go in operatorName instead. Registrations are written in uppercase
with no hyphens, so that G-VBOW and GVBOW do not describe two
different aircraft.
Working with surveillance data
The aircraft and operator fields are shaped to take ADS-B data without losing anything. A surveillance feed supplies a Mode S address, a registration, an ICAO type designator, a human-readable type description and a callsign, and each of those has a field of its own.
The callsign is written verbatim into callsign and is never decomposed on
the way in. UAL38 and FDX5608 split cleanly into an operator
and a flight number, so an importer can fill those as well. A general aviation aircraft
transmits its registration, and a state aircraft may transmit a bare word such as
LEGACY. Anything that cannot be split with certainty stays in
callsign alone.
Surveillance datasets usually expose a single combined owner-or-operator field, and in
practice its contents are the registrant rather than the operator. Much of the fleet is
registered to trusts and single-purpose holding companies, so a Pilatus PC-12 comes back
registered to a trust company and a Cessna 172 to an LLC named after its own tail number.
That value belongs in registeredOwner. Military and state aircraft usually
have no entry at all.
What Contrail leaves out
Duration, distance, and great circle mileage are all derivable from the times and places already present in a record. Storing them would guarantee that records written by different tools eventually disagree with each other.
Airport names, coordinates, and elevations are reference data about the world rather than
assertions by the author, so they do not belong in a personal repository. The one
exception is place.geo, which exists for landing sites that no identifier
system covers.
Times and time zones
Contrail defines eight timestamps, following the standard out, off, on and in convention, each with a scheduled and an actual variant. All of them are optional. A general aviation record will often carry only a takeoff and a landing.
Every timestamp is written with an explicit UTC offset and no fractional seconds. The Lexicon specification notes two round tripping ambiguities for datetimes, one involving lost precision and one involving trailing fractional second zeros, and a fixed convention avoids both.
Flighty importer demo
The demo below imports a Flighty CSV export into your repository as Contrail records. It runs entirely in this page. Your file is parsed in the browser and is never uploaded, because there is no server to upload it to.
Before you write anything
A complete flight history is also a movement history. Once these records are in your repository they are public and permanent, and a reader can work out where you are based, how often you travel, and when you are away from home.
This holds regardless of what the records claim. The relationship field
being optional does not change it, because records about flights sitting in your
repository disclose your connection to those flights either way.
Definitions
The following schemas are published to did:plc:5jsivedaksbcc4n2epiegflk and resolve by NSID.
com.airplaneian.contrail.temp.flightrecord, key: tidAn assertion that a particular flight existed. The record describes the flight itself rather than the author's experience of it, so it is not a personal log entry.
The author's connection to the flight is an optional annotation in 'relationship', and when that field is absent no claim is being made either way. Only 'createdAt' is required.
Every other field is optional, and an absent field means the value is unknown or does not apply, never that it is empty or zero.
Several people may write records describing the same flight and none of them is canonical, so write the identifying fields consistently wherever they are known: 'date', 'operator' and 'flightNumber' for scheduled service, 'icao24' with an actual departure time otherwise, and 'callsign' verbatim wherever a source supplies one.
There is no global flight identifier, because no authority exists to issue one. Any kind of flying is in scope, from scheduled airline service through cargo, charter, state and military operations to general aviation, and origin may equal destination.
Values derivable from other fields, such as duration and distance, are excluded on purpose, because tools computing them separately will disagree.
Reference data about the world, such as airport names or coordinates looked up from a code, does not belong in a personal repository either.
Note that a collection of these records is a movement history, and discloses the author's travel patterns regardless of what any individual record claims.
createdAtstring:datetimerequiredWhen this record was written, as distinct from when the flight happened. The only required field. Write it with an explicit UTC offset and no fractional seconds, for example '2026-07-24T18:22:05-07:00' or '2026-07-25T01:22:05Z'.
Fractional seconds are omitted throughout this schema because trailing zeroes in a fractional part round-trip ambiguously and lose precision inconsistently between implementations; a fixed convention avoids both problems.
datestringCalendar date of departure at the origin, as 'YYYY-MM-DD'.
Present as its own field rather than derived from the timestamps because it forms one third of the commercial identity key, alongside 'operator' and 'flightNumber', and because deriving it from an instant requires knowing the origin's local offset, which a reader may not have.
Where both this and a departure timestamp are written they must agree. Write it consistently, since cross-repository matching depends on it.
origincom.airplaneian.contrail.temp.defs#placeWhere the flight departed. May equal 'destination': local flights that return to their departure point, such as circuits, sightseeing, or training sorties, are ordinary and must not be treated as an error.
destinationcom.airplaneian.contrail.temp.defs#placeWhere the flight arrived, or where it was intended to arrive if it was cancelled. If the flight diverted, this remains the intended destination and the actual landing place goes in 'diversionAirport'.
sourcestringShort identifier for the tool or dataset that produced this record, for example 'flighty', 'manual', or 'adsb'. Together with 'sourceId' this lets a tool find the records it wrote previously and update them in place instead of creating duplicates on a re-import.
It also records provenance: the same record shape carries a first-hand passenger entry and a machine-harvested observation, and 'source' is what distinguishes them.
sourceIdstringThe identifier the tool named in 'source' uses for this flight, in that tool's own namespace, typically an opaque key from an export.
Deliberately stored in the record body rather than encoded into the record key, so that a record can be corrected without changing its identity: deriving keys from content would turn a corrected flight number into a new record instead of an update.
Meaningful only in combination with 'source', and carries no meaning to readers who do not know that tool.
callsignstringThe flight identification the aircraft transmitted, written exactly as received, uppercase with no padding or spaces.
This is the primary flight identifier available from ADS-B and other surveillance sources, and it must not be decomposed on the way in: 'UAL38' splits cleanly into an operator and a flight number, but a general aviation aircraft transmits its registration, and state or military aircraft commonly transmit something with no civil structure at all, such as 'LEGACY' or 'KLH71'.
Write this field verbatim and populate 'operator' and 'flightNumber' as well only when the split is unambiguous. Absent for sources that never see a callsign, such as a passenger's own booking history.
operatorstringICAO designator of the agency that actually operated the flight, three letters, uppercase. Not restricted to airlines: 'UAL' for an airline, 'FDX' for a cargo carrier, 'RCH' for a military air mobility flight.
ICAO rather than IATA because IATA codes are recycled between carriers, so a historical record tagged with one is ambiguous. Absent for most general aviation, where no designator exists; use 'operatorName' in that case.
Part of the commercial identity key alongside 'date' and 'flightNumber', so write it consistently where it is known, since cross-repository matching depends on it.
operatorNamestringFree-text name of the agency that operated the flight, for operators that have no ICAO designator: flight schools, charter and air taxi businesses, law enforcement and government units, private owners flying their own aircraft.
Not normalised and not reliable for matching, in the same way that 'aircraftType' complements 'icaoTypeDesignator'. Do not restate here the name of an operator whose designator is already in 'operator'. Note that this is who flew the aircraft, which is frequently not who owns it.
flightNumberstringThe operating agency's flight number, as a string rather than an integer. Alphabetic suffixes are in real use, some sources emit malformed values, and a numeric type could never be loosened later without breaking every existing record.
Write the digits alone without the operator designator prefix. Part of the commercial identity key, so write it consistently, since cross-repository matching depends on it.
Absent for flights that have no flight number, which is most of general aviation and much state and military flying.
marketingAirlinestringICAO designator of the airline that sold the flight, when it differs from 'operator'. This one is deliberately named for airlines, because codeshare marketing is an airline arrangement and has no counterpart in other kinds of operation.
Absent means either that there was no codeshare or that it is unknown. Do not put the marketing carrier in 'operator'; the distinction is what lets two records describing one physical flight be recognised as such.
marketingFlightNumberstringFlight number under which the flight was sold, when it differs from 'flightNumber'. A string, for the same reasons.
registrationstringRegistration or tail number of the airframe, for example 'N12345' or 'GVBOW'. Write it uppercase with no hyphens, spaces, or other separators, so that 'G-VBOW' and 'GVBOW' do not become two different aircraft to a reader.
This normalisation matters because registration is one of the few fields by which general aviation records can be matched across repositories. Military and state aircraft carry identifiers in this field too, in whatever form their operator uses, such as 'MM62209' or 'FAC1215'.
Note that registrations are reassigned to different airframes over time, so 'icao24' is the more durable airframe identifier where it is known.
registeredOwnerstringFree-text name of the party the airframe is registered to, as the civil registry records it.
This is deliberately separate from 'operator' and 'operatorName', because the registrant is frequently not the operator: a large share of the fleet is registered to trusts, banks, or single-purpose holding companies, such as 'WILMINGTON TRUST CO TRUSTEE' or a limited company named after the tail number.
Surveillance datasets usually expose a single combined owner-or-operator field, and its contents are the registrant, so it belongs here rather than in 'operator'. Absent for aircraft with no public registry entry, which includes most military aircraft.
aircraftTypestringFree-text description of the aircraft type as the source expressed it, for example 'Boeing 737-800' or 'Piper PA-18 Super Cub'.
Present because most sources supply a name rather than a designator, and discarding it in favour of an empty 'icaoTypeDesignator' would lose real information. Not normalised and not reliable for matching, so use 'icaoTypeDesignator' for that.
Do not derive one field from the other, because a guessed designator is worse than an absent one and several distinct designators share a single marketing name.
icaoTypeDesignatorstringICAO aircraft type designator, for example 'B77W' or 'C172'. Write it only when the source actually supplies a designator, not when it has been inferred from a marketing name.
icao24stringThe airframe's 24-bit ICAO address as six lowercase hexadecimal characters, for example 'a1b2c3'. Stable per airframe and the primary identity field for general aviation, where there is no flight number to match on.
Together with an actual departure time it forms the general aviation identity key; write both consistently, since cross-repository matching depends on them.
scheduledGateDeparturestring:datetimeScheduled time the aircraft left the gate or parking position, the 'out' time. Write it with an explicit UTC offset and no fractional seconds. The eight time fields form the standard out, off, on and in set.
All of them are optional, so a record knowing only a takeoff time is valid, and general aviation records will usually omit the gate times entirely.
actualGateDeparturestring:datetimeActual time the aircraft left the gate or parking position, the 'out' time. Explicit UTC offset, no fractional seconds. The interval between this and 'actualGateArrival' is block time, which is not stored because it is derivable.
scheduledTakeoffstring:datetimeScheduled wheels-off time, the 'off' time. Explicit UTC offset, no fractional seconds.
actualTakeoffstring:datetimeActual wheels-off time, the 'off' time. Explicit UTC offset, no fractional seconds. Together with 'icao24' this forms the general aviation identity key, and it is usually the most reliably known time for a flight with no gate operations.
scheduledLandingstring:datetimeScheduled wheels-on time, the 'on' time. Explicit UTC offset, no fractional seconds.
actualLandingstring:datetimeActual wheels-on time, the 'on' time. Explicit UTC offset, no fractional seconds. The interval between this and 'actualTakeoff' is flight time, which is not stored because it is derivable.
scheduledGateArrivalstring:datetimeScheduled time the aircraft reached the gate or parking position, the 'in' time. Explicit UTC offset, no fractional seconds.
actualGateArrivalstring:datetimeActual time the aircraft reached the gate or parking position, the 'in' time. Explicit UTC offset, no fractional seconds.
relationshipstringpassengerpicsicstudentinstructorcrewobserverThe author's connection to this flight. Unlike every other field in this record, absent here does not mean unknown: it means no claim of any relationship is being made. A record written from ADS-B data about someone else's flight simply omits it.
An open string rather than a closed set, so that relationships nobody anticipated can be expressed without a schema change, and readers must tolerate values outside the known list.
Note that omitting this field does not make a record private, because the presence of flight records in a repository discloses the author's connection to those flights regardless of what the records claim.
seatstringSeat occupied, as a string, for example '14C', with row and position together, since the letter is not optional and the whole value is not a number. Only meaningful when 'relationship' indicates the author was aboard.
cabinstringeconomypremiumEconomybusinessfirstCabin or class of service. An open string rather than an enumeration because carriers invent and rename cabins continually and any closed set would be obsolete within a year.
Write lower camel case, matching the known values below, so that records from different tools compare equal; readers must tolerate values outside the list.
statusstringnormalcancelleddivertedHow the flight concluded. Absent means unknown rather than normal, so write 'normal' explicitly when it is known that the flight operated as planned. An open string for consistency with the other classification fields here; readers must tolerate values outside the list.
diversionAirportcom.airplaneian.contrail.temp.defs#placeWhere the flight actually landed, when it did not land at 'destination'. Present only when 'status' is 'diverted'. 'destination' continues to hold the intended arrival point.
notesstringFree-text note about the flight. Everything written here is public and permanent once the record is published, and unlike the structured fields its contents cannot be reviewed by a tool for anything the author did not mean to disclose.
Tools writing records on a user's behalf should show note contents prominently before publishing. Do not use this field to carry structured data that belongs in a dedicated field, and do not use it for booking references or other credentials.
com.airplaneian.contrail.temp.triprecord, key: tidAn assertion that a set of flights belonged to one journey. Grouping is a separate record so that a flight record stays a standalone assertion that does not depend on any grouping existing, and so that deleting a trip orphans nothing.
The reference points one way only, from trip to flight, because nothing in the protocol keeps two directions consistent. A trip references only flight records the author wrote, and makes no claim about anyone else's repository.
Only 'createdAt' is required, and an absent field means unknown or not applicable. Values derivable from the referenced flights, including start and end dates, leg count and total distance, are excluded on purpose.
Note that a trip discloses more than its flights do individually, because it asserts that particular journeys were a single continuous absence.
createdAtstring:datetimerequiredWhen this record was written, as distinct from when the trip happened. The only required field. Write it with an explicit UTC offset and no fractional seconds, for example '2026-07-25T18:22:05-07:00'.
namestringFree-text name for the trip, for example 'Tokyo, spring 2026'. Optional, and as with any free text it is public and permanent once published.
flightsarray<string:at-uri>The flights that made up this trip, in the order they were flown, as AT-URIs pointing at com.airplaneian.contrail.temp.flight records.
Plain AT-URIs rather than content-pinned strong references, because flight records are expected to be corrected in place as better data arrives, and a pinned reference would break on every such correction.
References may dangle if a flight is later deleted; readers must tolerate that rather than treat it as an error.
sourcestringShort identifier for the tool that produced this record, for example 'flighty' or 'manual'. Same meaning as on a flight record: together with 'sourceId' it lets a tool update the trips it wrote previously instead of duplicating them on a re-import.
sourceIdstringThe identifier the tool named in 'source' uses for this trip, in that tool's own namespace. Note that a source's grouping key may be sensitive even when the resulting grouping is not.
A booking reference, for instance, is a credential that can be used to retrieve a reservation, and must not be written here. Derive an opaque identifier instead where the natural key cannot be published.
com.airplaneian.contrail.temp.defs#placeobjectAn airport, airfield, or landing site referenced by a flight record. Every field is optional, and absent always means unknown or not applicable, never empty or a placeholder.
A grass strip with no published identifier is a valid place carrying only 'name' and 'geo', while a major airport may carry every field.
Write the identifier fields wherever they are known, because readers reconciling records of the same flight written by different people match on them. Prefer 'icao' where a place has one, since IATA codes are reassigned over time and many airfields have no IATA code at all.
Never invent, translate, or derive an identifier the source did not supply, because an absent code is better than a wrong one. This def describes a place only as far as a flight record needs to, and is not a general-purpose airport reference entry.
icaostringICAO location indicator, four characters, uppercase. For example 'KOAK' for Oakland or 'EGLL' for Heathrow. Preferred over 'iata' for cross-repository matching because ICAO indicators are globally unique and are not reassigned.
Many small airfields have none; omit the field rather than guessing.
iatastringIATA location code, three characters, uppercase. For example 'OAK'. Absent for the great majority of general aviation fields.
Note that IATA codes are reassigned between airports over time, so a code alone does not identify a place across decades, and it should be paired with 'icao' or 'geo' where possible.
faaLidstringFAA Location Identifier, for United States airfields that have one. For example '1G5'. Many US public-use fields have an FAA LID and no ICAO indicator at all, which is why this is a separate field rather than a fallback written into 'icao'.
namestringFree-text name of the place, for landing sites that no identifier system covers: grass strips, private fields, off-airport landings, water landings. Also useful as a human-readable label alongside a code.
Carries no matching guarantees, since two records may name the same field differently.
geocommunity.lexicon.location.geoCoordinates of the place, for landing sites that no identifier system covers, or to disambiguate an ambiguous name. Note that the referenced def requires both latitude and longitude, expressed as decimal-degree strings referenced to WGS84.
See the community.lexicon.location documentation for the coordinate conventions. Do not write coordinates that were merely looked up from an identifier already present in this object, because that is reference data about the world rather than an assertion about this flight.
terminalstringTerminal used by this particular flight at this place, as free text, since terminal naming is entirely carrier- and airport-specific ('3', 'B', 'Main', 'INTL').
This describes how this flight was handled rather than a property of the place itself, which is why it lives here rather than in any airport reference dataset.
gatestringGate or stand used by this particular flight at this place, as free text ('C19', '44C', 'A2E'). As with 'terminal', this is a fact about this flight rather than about the place. Not applicable to most general aviation operations.
These are currently published under a temp namespace while the shape
settles. Published lexicon constraints can never be loosened afterwards, so the stable
names will not be claimed until real data has been through them.