Consensus flow list
On this page

File schemas

Field-by-field reference for the record shapes the pipeline reads and writes. For advice on which output you want, see Which output do I need?.

Files live in the data directory — ~/Library/Application Support/consensus-flow-list/ on macOS, overridable with CONSENSUS_FLOW_LIST_DATA_DIR.

Three of these are record shapes, not files

elementary-flows.json, flow-objects.json and harmonised-flows.json are no longer written. The records they described are: they are what consensus-flows.sqlite3 stores in flow_objects.flow_object_json and elementary_flows.flow_json, and their JSON Schemas are still generated and still enforced. The ElementaryFlow record is no longer stored: it was a second copy of flow_json, identical on every shared key in all 94,433 rows, and pipeline.sqlite.elementary_flow_record projects it.

The sections below are kept under their old filenames because that is what the schema files are called and what an older data directory contains. Read them as descriptions of the record, and see Which output do I need? for where it now lives.

Machine-readable schemas are the authority

JSON Schemas for the published artifacts are checked in under src/consensus_flow_list/data/schemas/:

  • elementary-flows.schema.json
  • flow-objects.schema.json
  • harmonised-flows.schema.json
  • harmonised-flows-simple.schema.json
  • lcia-factors.schema.json
  • lcia-differences.schema.json

They are generated from the record classes by domain/schema.py, and tests/test_schemas.py fails if the checked-in copies drift from those classes or if a real artifact does not validate. Prefer them over this page when the two disagree — and please fix this page when they do.

This page exists to explain what the fields mean, which a schema cannot.

schema_version

Every wrapped file carries schema_version, currently 6 for the flow artifacts. It is the signal a reader uses to tell "this artifact does not have the field I want" from "this artifact is older than that field": domain.schema.check_schema_version raises on a version it cannot read, rather than letting a caller merge against fields that have moved.

The two LCIA artifacts are versioned separately, by LCIA_SCHEMA_VERSION, currently 1. lcia-factors.json.gz and lcia-differences.json describe characterisation factors rather than flows, and a flow-list bump has nothing to say about a factor; a reader of one passes expected=LCIA_SCHEMA_VERSION to check_schema_version. That one number covers both the files and the lcia_* tables, because both are renderings of the same records.

Nothing in the pipeline calls it yet

check_schema_version has no production call site — only tests/test_schemas.py. A consumer that reads these artifacts should call it; this project writes them and does not currently read one back. This note exists because an earlier version of this page said the check was wired into the merge, and it is not.

Bump it only when a change would break a reader. Adding an optional field to a record does not; adding a top-level key to a document does, because the documents are additionalProperties: False.


elementary-flows.json

Written by build.

{
  "schema_version": 1,
  "stats": {  },
  "elementary_flows": [ <ElementaryFlow>,  ]
}

ElementaryFlow

Field Type Description
elementary_flow_id string (UUID) Identifier for this substance-in-context
flow_object_id string (fo-<16 hex>) The substance this flow is an occurrence of
source string Source label, e.g. "EF 3.1"
context object Structured consensus context
context_iri string (IRI) Canonical context identifier — the authoritative form
unit string | null Canonical unit notation, e.g. "kg"
unit_iri string (IRI) | null Canonical unit identifier
lcia_methods list[object] Characterisation factors carried from the source list. Each states the method, the category and the number; 42,871 of EF 3.1's 319,575 also state a geography, the place the number applies to
general_comment string | null Free text from the source list
cas_match_labels object CAS number → SKOS match-quality IRI
concept_associations list[ConceptAssociation] Links to source-list flows. Absent when unset
owl:deprecated boolean Present only on deprecated flows
dcterms:isReplacedBy string (IRI) Present only on deprecated flows
is_replaced_by_uuid string Present only on deprecated flows

Required on every record: elementary_flow_id, flow_object_id, source, context, context_iri, unit, unit_iri, lcia_methods, general_comment, cas_match_labels. The remaining four are omitted when unset rather than serialised as null, so an active flow does not carry empty deprecation keys.

Source-specific keys not listed here are round-tripped verbatim at the top level, so an unrecognised input field is preserved rather than dropped.

cas_match_labels maps each CAS number to how confidently it matched — skos:exactMatch, skos:closeMatch, or skos:relatedMatch.

Invariant: (flow_object_id, context_iri) is unique across all non-deprecated elementary flows.

source_refs is not in this file

Elementary flows carry source_refs in memory, but the writer strips it — the same applies to harmonised-flows.json. Per-source detail is available from the elementary_flow_sources table in consensus-flows.sqlite3 and, for merged flows, from the merge report.

This is a known gap between the record classes and the published files, not a documentation error: the field is required by the class and absent from the artifact. The generated schemas reflect the artifact.

elementary_flow_sources is the only home for a flow's references

Not just the published files: no stored payload carries source_refs either. elementary_flows.flow_json held a copy until #252, and it was the build-time answer — the merge appends a matched flow's new reference to elementary_flow_sources and to nothing else, so on the 2026-08-07 build 7,794 of 94,433 stored copies were short: the payload named the base list's single source where the table named two to 38. None was ever wrong the other way: no payload held a reference the table lacked.

Read references from the table. A payload is not a fallback for it.

Every record in this file now has the same shape, whichever path produced it. Flows added by the merge used to be built as raw dicts and omitted fields that transform-created flows carried; they are now built through the record classes, so one schema covers both.

SourceRef

Not present in the published layered file — see the warning above. This is the shape it has in memory, in the elementary_flow_sources SQLite table, and in the merge report.

{
  "list_name": "EF",
  "list_version": "3.1",
  "source_flow_uuid": "0000b186-aea3-4c0a-b0c2-c284de7cdf92",
  "source_flow_name": "…",
  "source_metadata": {
    "input_file": "/…/ef-31-flows.json",
    "input_dataset": "EF 3.1",
    "original_context": ["Emissions", "Emissions to air", "Emissions to air, indoor"]
  }
}

source_flow_name and original_context are verbatim as the source list wrote them, not normalised.

ConceptAssociation

{
  "@type": "xkos:ConceptAssociation",
  "xkos:sourceConcept": {
    "@id": "https://vocab.brightway.one/ef/3.1/flow/<uuid>",
    "http://www.w3.org/2004/02/skos/core#prefLabel": "<source flow name>",
    "context": "Emissions/Emissions to air/Emissions to air, indoor",
    "qudt:hasUnit": {"@id": "https://vocab.brightway.one/units/unit/KiloGM"},
    "http://www.w3.org/2004/02/skos/core#exactMatch": {"@id": "<target IRI>"}
  },
  "xkos:targetConcept": {"@id": "<target IRI>"},
  "provenance": {  },
  "qudt:conversionMultiplier": 9.41
}

There is no xkos:mapType. XKOS defines no property for the type or strength of a mapping — the specification says so explicitly. The match kind is carried on the source concept as the SKOS mapping property itself. Readers should look for whichever of skos:exactMatch, skos:closeMatch, skos:broadMatch or skos:relatedMatch is present.

skos:broadMatch is used for manual ecoinvent additions, where one consensus flow groups several source flows. Everything else uses skos:exactMatch.

qudt:conversionMultiplier

Multiply an amount of the source flow by it to get the amount of the target flow.

It is published only where units.json could not already have supplied the factor. Litres to cubic metres, tonnes to kilograms, MJ to J: those are properties of the units themselves, carried on the unit via conversion_multiplier and reference_unit_iri, and restating them per mapping would be a second copy of a fact that has a home. Two kinds of conversion have no such home, and both are here.

Crossing quantity kinds. Mass to Energy, Volume to Mass. There is no unit-table factor to defer to, because how many megajoules a kilogram is depends on the substance. Seven ecoinvent flows, in every version from 3.8 to 3.12:

ecoinvent flow consensus flow Multiplier
Coal, brown (kg) Brown Coal (MJ) 9.41
Coal, hard (kg) Hard Coal (MJ) 18.01
Oil, crude (kg) Crude Oil (MJ) 43.4
Gas, natural (Sm3) Natural Gas (MJ) 36.0
Gas, mine, off-gas (Sm3) Natural Gas (MJ) 36.0
Uranium (kg) Uranium (MJ) 560000.0
Water, salt, ocean (m3) Sea Water (kg) 1025.0

Changing what is measured. ecoinvent gives the ore; EF 3.1 gives the metal. Both sides are in kilograms, so the units raise no objection at all, and the unit table's answer for the pair — 1.0 — is the wrong one. 0.599 is titanium's share of titanium dioxide by mass (47.867 / 79.866):

ecoinvent flow consensus flow Multiplier
TiO2, 54% in ilmenite, 2.6% in crude ore (kg) Titanium (kg) 0.599
TiO2, 54% in ilmenite, 18% in crude ore (kg) Titanium (kg) 0.599
TiO2, 95% in rutile, 0.40% in crude ore (kg) Titanium (kg) 0.599

ecoinvent replaced the ore flows with elemental Titanium at 3.10.1, so these three appear in 3.8 and 3.9.1 only. Barite, 15% in crude ore is deliberately not here: it maps onto EF 3.1's baryte, which is the same compound, so no conversion applies. It would need one only against elemental barium, which is not the target this project takes.

Crossing a time dimension, and only where a curator said so. Three obsolete ecoinvent land-occupation flows are published in m² against an m²·a flow:

ecoinvent flow consensus flow Multiplier
Occupation, arable, conservation tillage (obsolete) (m2) arable (m2·a) 1.0
Occupation, arable, conventional tillage (obsolete) (m2) arable (m2·a) 1.0
Occupation, arable, reduced tillage (obsolete) (m2) arable (m2·a) 1.0

A factor across a time dimension asserts a duration, which is a modelling choice rather than a fact about the land. Every ecoinvent correspondence table states 1.0 for these, commented "Assumed conversion based on land use through an entire year" — and the merge refuses a time-crossing factor that arrives from a table, so that assumption is never inherited silently. The same number is asserted in ecoinvent-match-overrides.json instead, where it carries its reasoning: the occupation lasted one year. An inventory using these flows for a shorter or longer occupation is mischaracterised by exactly the ratio, which is why ecoinvent made them obsolete.

One source list, one flow, two units. A list can ship the same flow twice in two spellings of one scale. BAFU does: Noise, Road, Lorry, Average in kilometres and again in metres, and the same for the passenger car. Both rows land on one consensus flow, because (flow_object_id, context_iri) is unique, and the flow has to declare one unit.

It declares the one units.json makes the reference unit for that quantity kind — the metre for a length — so the answer does not depend on which row the merge read first. It used to: the lorry came out in metres and the passenger car in kilometres, from one list, on one day.

Where none of the units offered is a reference unit, the table has no opinion and a curator writes one into created-flow-unit-decisions.json, naming the row whose unit wins. BAFU's Heat, waste is the case that exists: megajoules against kilowatt-hours, where the coherent unit for an energy is the joule and BAFU offers neither. Its entry chooses megajoules, because that is what BAFU itself writes in the other thirteen compartments it ships the flow in. Where neither a decision nor a reference unit settles it, the first row stands and the run says so — and the row is listed under Checks → Unit disagreements as needing a decision.

Either way, a row whose own unit is not the one the flow states is flagged has_unit_mismatch on its merge outcome, which means on a creation exactly what it means on a match: this source row is measured in something other than the flow it landed on. Which of the two situations produced it is the outcome's own outcome column.

Each row keeps its own unit where its own unit belongs: on its source ref and on its qudt:hasUnit. No multiplier is published for the difference, by the rule above — kilometres to metres is exactly what the unit table already says. A group whose units share no reference unit is left as it arrived and reported, because choosing there would be picking rather than deciding.

One source list, one resource, two quantity kinds. The harder version of the same case, where the two units are not one scale. BAFU ships standing wood by volume and again by mass, and no reference unit joins a cubic metre to a kilogram. There the list is rebased by hand, in that list's manual fixes, and the fix states the factor:

BAFU flow consensus flow Multiplier
Wood, unspecified, standing/kg (kg) Wood, Unspecified, Standing (m3) 0.00204

0.49 oven-dry tonnes per cubic metre of fresh volume, a 50:50 mix of Picea abies and Fagus sylvatica from Table 4.14 of the 2006 IPCC Guidelines. The mapping states kg, because that is the unit BAFU's amounts are in; the flow is in m³, because that is what every other standing-wood flow in every list uses. This is the only kind of multiplier that does not come from a correspondence table — BAFU publishes none — and it is why one can be authored on a fix at all. See Rebasing a row onto another unit.

Read the multiplier with the units on both ends: qudt:hasUnit on the source concept, and the target flow's own unit. The number is bare, so a consumer that assumes one pairing gets the others wrong — 36.0 is per standard cubic metre, not per kilogram, and 1025.0 is a density rather than a calorific value.

Absence does not mean 1.0. It means no factor was stated. A pair whose units disagree and which carries no multiplier is either a reviewed exception in unit-change-allowlist.json or a defect; treating it as parity is the fossil-depletion underestimate of #257. One such pair exists today, documented rather than converted: ecoinvent's Manganese-55 (kBq) onto elemental manganese in kg, where 55Mn is manganese's stable isotope and so has no activity to convert by. No ecoinvent 3.8 dataset uses the flow, so nothing an inventory carries is affected.

The values are the EF 3.1 LCIA method as implemented by the ecoinvent Centre, reaching this project through the randonneur_data correspondence tables. They are not in the JRC's own EF 3.1 distribution: every one of the target flow datasets in EF-v3.1.zip carries a single flow-property meanValue of 1.0, and no characterisation factor in its LCIA methods has these values. The ecoinvent implementation is where they exist.

What none of them states is a basis — a calorific value is net or gross, and 26 MJ/kg dry-ash-free is a different claim from 26 MJ/kg as-received. Uranium's is documented as a gross (HHV) fission energy content; the coal and oil values say nothing either way. Giving the field a unit, an explicit basis and provenance is tracked in #70.

It never appears on a flow or a flow object. A factor is a statement about a pair — 9.41 is not a fact about brown coal until it is 9.41 MJ per kg — so it belongs to the mapping and to nothing else.

Source IRI prefixes:

Source Prefix
EF 3.1 https://vocab.brightway.one/ef/3.1/flow/
SimaPro Professional 10.2 the SimaPro 10.2 flow prefix
Consensus target https://vocab.brightway.dev/elementary-flows/

stats

Counts from the run that produced the file, present on both layered files:

flow_object_count, elementary_flow_count, override_hits, cas_conflict_guard_hits, nuclide_object_hits, nuclide_object_count, element_flow_object_count_added, element_flow_object_names_added, element_flow_object_count_enriched, element_flow_object_count_ignored_unlinked, isotope_flow_object_count_added, isotope_kbq_candidate_flow_object_count, isotope_kbq_matched_flow_object_count, isotope_kbq_unmatched_flow_object_count, isotope_kbq_unmatched_reasons, isotope_kbq_withheld_flow_object_count, isotope_kbq_withheld_reasons, isotope_rows_unparsed_count, isotope_unlinked_element_count, isotope_unlinked_elements, short_lived_isotopes_not_in_consensus_count, monoatomic_ion_flow_object_count_marked, monoatomic_ion_charge_detected_count, monoatomic_ion_cas_updates, monoatomic_ion_ec_updates, duplicate_elementary_group_count, deprecated_elementary_flow_count.

The isotope counts split three ways rather than into matched and unmatched. A label that is not a nuclide name is the expected outcome for an aggregate; a nuclide the tables have never heard of is a correction waiting to be written; and a record that failed its own consistency checks is neither. One number covered all three, which is how 37 wrong records and one unresolvable label sat behind the same figure. isotope_kbq_unmatched_reasons and isotope_kbq_withheld_reasons are objects keyed by reason, and isotope_unlinked_elements is keyed by element name. See What kind of thing is this flow?.


flow-objects.json

Written by build.

{
  "schema_version": 1,
  "stats": {  },
  "flow_objects": [ <FlowObject>,  ]
}

FlowObject

Field Type Description
flow_object_id string (fo-<16 hex>) Stable identifier, a hash of the identity that produced it
prefLabel list[LangString] Preferred name(s), {"@value": "…", "@language": "en"}
altLabel list[LangString] Synonyms
properties object keyed by IRI Chemical properties; see below
references list External database references
created_from object How this object was assembled
classifications object Registry numbers (CAS, EC, KEGG, Gmelin) with resource URLs and per-value provenance
origin_qualifier string | null See below. null for base substances
parent_flow_object_id string | null The base substance, for qualified flows. null for base substances
skos:definition list Prose definitions. Omitted when unset
@type list[string] Semantic types, e.g. chemrof:FullySpecifiedAtom. Omitted when unset

origin_qualifier is one of biogenic, fossil, land_use_change, biogenic_resource_correction, biogenic_100yr, green_water, blue_water, grey_water, alpha_emitters, delayed_emission_correction, biogenic_delayed_emission_correction, fossil_delayed_emission_correction. See Flow objects and elementary flows.

Property keys are ChemROF IRIs:

IRI Meaning
https://w3id.org/chemrof/molecular_formula Molecular formula
https://w3id.org/chemrof/molecular_mass Molecular mass
https://w3id.org/chemrof/elemental_charge Charge; non-zero identifies an ion
https://w3id.org/chemrof/atomic_number Present on elements
https://w3id.org/chemrof/inchi2d_string InChI
https://w3id.org/chemrof/inchi2d_key_string InChIKey
https://w3id.org/chemrof/smiles_string SMILES
https://w3id.org/chemrof/iupac_name IUPAC name

Property values are wrapped as {"@value": …} with provenance alongside. A value may be a list where several sources contributed — see Known limitations, which explains why that is a problem worth knowing about.


harmonised-flows-simple.json.gz

Written by build, updated by build. Gzip-compressed. The published export — this is what downstream consumers should read.

{
  "@context": {  },
  "schema_version": 5,
  "flows": [ <SimpleFlow>,  ],
  "redirects": [ <FlowRedirect>,  ],
  "concept_schemes": [ <ConceptScheme>,  ],
  "correspondences": [ <Correspondence>,  ]
}

Six top-level keys, all required. concept_schemes and correspondences hold the mappings to other flow lists, which moved out of the flows in schema version 4; see JSON-LD.

Deprecated flows are excluded from flows. Every flow has the same fields regardless of source.

FlowRedirect

One per deprecated flow: which surviving flow its identifier resolves to, and whether the two were ever the same flow. Added in schema version 5 (#265).

Field Type Description
identifier string (UUID) The deprecated identifier a consumer is holding
replaced_by_identifier string (UUID) The surviving flow, as a bare UUID. Terminal — if a replacement were itself deprecated later this is still the end of the chain, not the next hop
@id string (IRI) The deprecated flow as an IRI
http://purl.org/dc/terms/isReplacedBy object {"@id": …} — the same survivor as an IRI
http://www.w3.org/2002/07/owl#deprecated boolean Always true
https://vocab.brightway.one/terms/deprecationReason object {"@id": …} — one of context-collapse, identity-merge, unclassified under https://vocab.brightway.one/deprecation-reasons/

The key is always present, and empty rather than absent when nothing was deprecated: a missing redirects would leave a consumer unable to tell "nothing was deprecated" from "an export written before redirects existed", which is the ambiguity the key exists to remove.

The reason is not decoration — read it before following a redirect

identity-merge means every source list both flows came from put them in the same context: the same flow reached the list twice, and following the redirect resolves an identifier rather than crossing a distinction. On the 2026-08-12 build that is all 130 redirects.

context-collapse means a list both came from put them in different contexts that map to one consensus context. The flows compared equal because the consensus context vocabulary cannot tell them apart, but their characterisation factors legitimately disagree — see limitations and #262. A consumer that follows one of these and overwrites is layering a second arbitrary choice on the first; the honest response is to detect it and refuse.

unclassified means the two share no source list that records a context, so they could not be compared. Treat it as unsafe, not as either of the above.

None of the three is a statement about the factors themselves: a source list that published one place twice with different numbers yields an identity merge whose factors still disagree (#49).

The reason is decided from elementary_flow_sources, which is where a flow's references live (#252) — not from the flow payload, whose copy was short on 7,794 of 94,433 flows before it was removed.

SimpleFlow

Field Type Description
identifier string (UUID) Always present. The ILCD UUID for EF flows; the elementary_flow_id for flows added during a merge
source string e.g. "EF 3.1", "ecoinvent algorithm addition"
cas_numbers list[string]
ec_numbers list[string]
context_iri string (IRI)
unit string Required — export fails if any flow lacks one
unit_iri string (IRI)
prefLabel string Plain string, language resolved (English preferred)
altLabel list[string] Plain strings, deduplicated case-insensitively
properties object Keyed by the full ChemROF IRI (https://w3id.org/chemrof/molecular_formula), read as plain JSON. Values unwrapped from @value, so a string or a number rather than an object. The @context declares a short name per term and maps this key to @nest, so a JSON-LD processor that compacts the document sees molecular_formula on the flow itself and no properties key at all
references list[string] Reference IRIs
definition list[string] Definition text
https://vocab.brightway.one/terms/originQualifier object | null {"@id": …} naming why this substance is held apart from one it shares a CAS number with. Omitted for the unqualified substance, which is most of them
https://vocab.brightway.one/terms/baseSubstance object | null {"@id": …} naming the substance it was held apart from, as a flow-object IRI. Omitted wherever originQualifier is, and where the qualified object has no CAS number to resolve a parent through
http://purl.obolibrary.org/obo/RO_0000087 list[object] | null What the substance is used for, as RO:0000087 has role onto ChEBI role classes. Each row is {"@id": …, "rdfs:label": …}; the definition and per-assertion provenance the flow-object layer carries are dropped here. A list, because a role is many-valued — sulfluramid is an insecticide and an acaricide. Omitted, not empty, where ChEBI has nothing to say: [] would read as "checked, and it bears none"

No concept_associations: the links to source-list flows left the flow in schema version 4 and are the top-level concept_schemes and correspondences, one correspondence per source list.

Present in harmonised-flows.json but not here: lcia_methods, uuid, elementary_flow_id, name, context, general_comment, synonyms, cas_match_labels, flow_object_id, _sources, input_datasets.

Note the shape difference from the layered files: labels are plain strings rather than language-tagged objects, and property values are unwrapped from their {"@value": …} envelope, losing the provenance that goes with it. Property keys are not shortened — they are the same ChemROF IRIs the layered records use. This file is built for consumption, not for round-tripping.


harmonised-flows.json

No longer a file. Every flow after all processing, before layering. It was written as a flat list — several gigabytes, and a denormalised duplicate of elementary-flows.json joined to flow-objects.json — until #82. The record is still produced and still the one the published export is projected from: it is the flow_json column of elementary_flows in consensus-flows.sqlite3, read by pipeline.sqlite.read_published_flow_payloads.

That column no longer holds the whole record. The keys describing the substance rather than the occurrence — altLabel, properties, references, prefLabel, @type, the SKOS definition, and RO:0000087 roles — are stored once per flow object in flow_object_payloads, because 94,433 flows share 7,730 objects and a copy each came to 1.57 GiB. read_published_flow_payloads joins them back, and a key a flow still carries wins over the object's, so the record it returns is what the column used to hold. Reading flow_json directly, without that join, now gives a partial record.

The section keeps the old filename because that is what the schema file is called; see the warning at the top of this page.

{  }   -- one object per row, in `elementary_flows.flow_json`

Selected fields:

Field Type Description
uuid string Source flow UUID
identifier string Canonical identifier
name string | null Flow name
source string Dataset label
cas_numbers, ec_numbers list[string] Registry numbers
context list | object Source strings until context resolution, structured after
context_iri string (IRI) Resolved consensus context
unit, unit_iri string
synonyms list[string] Legacy synonym field from the source
lcia_methods list[object] Characterisation factors; a geography key where the source states one
input_datasets list[string] Datasets this row came from
prefLabel, altLabel list[LangString] Labels after enrichment
flow_object_id string Set during layering
properties object keyed by IRI
references list
concept_associations list[ConceptAssociation]
skos:definition list Omitted when unset
owl:deprecated, dcterms:isReplacedBy, is_replaced_by_uuid Omitted when unset
cas_match_labels object CAS → SKOS match quality
cas_number_sources object CAS → structured provenance
general_comment string
_sources object Field name → the processing step that last wrote it. Transform state — skip it
_transformed boolean Has the transformer chain already run over this flow? Transform state — skip it
_provided object What the source list shipped, before any transformer. Transform state — skip it

Unrecognised source keys are preserved at the top level.

Required on every record: _provided, _sources, _transformed, altLabel, cas_numbers, concept_associations, context, context_iri, ec_numbers, flow_object_id, identifier, input_datasets, lcia_methods, properties, references, source, synonyms, unit_iri, uuid.

The underscore keys are transform state — readers should skip them

_provided, _sources and _transformed are the transform stage's working state, and the underscore is the signal: they are in flow_json because that column is where the transform keeps its records, not because anything reading a flow wants them. The published export drops all three — _strip_lcia_from_flows in pipeline/exporting.py projects an explicit field list, and none of the three is on it — and the review webapp, which hands flow_json to its templates wholesale, renders none of them either.

If you are reading flow_json for the flow record, ignore every key that starts with an underscore. What does read them is the pipeline itself — the transformer chain and the merge, which run over these records rather than consume them — and that is exactly what makes them working state. A reader that treats them as part of the record is reading the pipeline's notes-to-self.

They are not free. Measured on the 2026-08-07 build, across all 94,433 rows, after #248 hoists the substance body into flow_object_payloads:

Key MiB % of flow_json
_provided 61.6 17.6
_sources 34.4 9.8
_transformed 1.7 0.5
Total 97.7 27.9

So a bit over a quarter of the 350.7 MiB the column holds is state no reader wants. That is the trade this page is recording rather than fixing: the keys stay where the pipeline writes them, and readers skip them (#251). Whether they belong in a table of their own, or on disk at all, is still open there.

_transformed and _provided

The chain is ordered for a single pass over raw input and is not idempotent: normalize_name_case runs fifth and re-title-cases labels that consensus_match sets at fourteenth. _transformed records that the chain has finished with a flow, so a later stage — the merge, which shows the transformers the consensus flows alongside each new source list — can hand both to the same loop without either re-transforming the consensus or having to say which side is which.

_provided holds name, synonyms, context, cas_numbers, ec_numbers and unit as the source list gave them. The pipeline replaces as well as adds: bootstrap_labels moves name into prefLabel and then purges name and synonyms, default_context_mapping rewrites context into a consensus context dict, and the CAS transformers substitute a different registry number for the one the list gave. Without this there is no way back to the input, and the merge — which matches on the source list's own names and keys its context lookup on the source list's own strings — has nothing to read.

It is not a copy of the input row: it holds what would otherwise be destroyed.

_sources

_sources maps a field name to the transformer that last wrote it. Unlike the other two it has no reader: apply_transformers writes it (pipeline/engine.py), and nothing in src/ reads it back — not the merge, not the export, not the webapp. The only code that looks at it is the test asserting it gets written.

It is also already stored elsewhere, twice over. The same loop that sets flow.pipeline_sources[field] appends a ChangeEvent for that identical edit, which is what changelog and changelog_flows are written from: changelog holds the transformer and field of every edit, changelog_flows holds which flows it landed on. _sources is that join with everything but the last write per field thrown away — 34.4 MiB restating, less completely, what 273,384 changelog rows already say.

That makes it the weakest of the three: the argument for keeping _provided is that the input is otherwise unrecoverable, and for _transformed that the chain reads it within the run, but neither applies here. It stays because #251 chose to document this state rather than move it, and dropping a persisted field is a schema change; it is the part of that issue most likely to be revisited.

source_refs is stripped from this file on write, exactly as it is from elementary-flows.json. For source traceability use the elementary_flow_sources table in consensus-flows.sqlite3, or the merge report for merged flows.

As with the layered file, every record now has the same shape regardless of whether the transform or the merge produced it.


Source list flows — input format

The shape ef-31-flows.json and every SourceList.flows_path are read in. There is no separate "additional input" any more: a list is a --source, and the same normaliser reads both sides. See Choosing sources.

Either shape is accepted:

[ <InputFlow>,  ]
{"flows": [ <InputFlow>,  ]}

(flow_data is also accepted as the wrapper key.)

InputFlow

Field Required Description
uuid yes Rows without one are dropped
name yes Flow name
source yes Dataset label; the list name and version are derived from it
context yes List of strings or a structured context object
unit yes Must resolve against the unit vocabulary
cas_numbers, ec_numbers, synonyms, prefLabel, altLabel, properties no Used if present

Non-object rows are ignored. A base file yielding no valid rows fails the run.


consensus-flows.sqlite3

A denormalised cache rebuilt on every transform, backing the review applications. Not a durable artifact — do not edit it.

Table Contents
flow_objects One row per substance, with JSON columns for labels, classifications, properties and references, plus origin_qualifier, parent_flow_object_id, flow_type
elementary_flows One row per occurrence, with context split into context_dimension, context_media, context_strata, context_indoor, context_population_density, context_geography, context_water_body, context_land_use, plus is_deprecated, replaced_by_uuid, lcia_factor_count, consensus_change_count. Its flow_json also carries the transform's own working state under underscore-prefixed keys, which a reader should skip
flow_object_payloads The substance-level keys of flow_json — labels, properties, references — stored once per flow object rather than once per elementary flow. A key is here only where every flow of that object agreed on it; one that disagreed keeps its own copy on the flow, and wins when the two are merged. properties cannot disagree — a flow's chemistry is derived from its substance (#345) — so that key is here for every object
elementary_flow_sources Flattened source_refs, and the only home for them: flow_json does not carry the field, because the merge appends here and to no payload (#252)
changelog The change log, every transformer, one row per edit: change_index, transformer, flow_object_id, field, old and new values, comment. Replaces the consensus_changes table, which held only the consensus_match slice
changelog_flows Which elementary flows each edit landed on: change_index, elementary_flow_uuid, flow_name, entity_version. An edit to a substance lands on every flow sharing it, so this is where the flow-level view comes from. entity_version is the edit's position in that flow's history, which is not the same order as change_index
provenance_activities A view, not a table: one PROV-O activity per (edit, flow), composed from changelog and changelog_flows. Every column was already in one of those two
filter_option_counts Precomputed facet counts for the /flows filters
flow_objects_fts, elementary_flows_fts FTS5 full-text indexes

Query examples are in Recipes.

Keyboard

?
Show or hide this map
Esc
Close

More as the sections are built.