Skip to content

sct ndjson

Convert an RF2 Snapshot directory into the canonical SNOMED CT NDJSON artefact.

This is the required first step - all other sct subcommands consume this output. It joins the RF2 files once, deterministically, and writes each active concept as a single line of JSON.


Usage

sct ndjson --rf2 <DIR|ZIP> [--rf2 <DIR|ZIP>...] [OPTIONS]

Options

Flag Default Description
--rf2 <DIR\|ZIP> (required) RF2 Snapshot directory or a .zip release archive. Repeat to layer extensions.
--locale <LOCALE> en-GB BCP-47 locale for preferred term selection.
--output <FILE> (derived from RF2 dir name) Output NDJSON path. Use -o - for stdout.
--include-inactive off Include inactive concepts (omitted by default).
--refsets <MODE> simple Which reference sets to load. simple loads concept-level Simple refsets (SCR exclusion, care connect, etc.); none skips them; all additionally loads ComplexMap, ExtendedMap, AttributeValue, and Association refsets. Payload-bearing rows are preserved in <stem>.refsets.ndjson; history is written to <stem>.history.ndjson. all is larger and slower and therefore requires file output rather than -o -. See cross-terminology mapping.

Examples

UK Monolith from a downloaded zip (no manual extraction needed)

sct ndjson --rf2 SnomedCT_MonolithRF2_PRODUCTION_20260311T120000Z.zip
# Output: snomedct-monolithrf2-production-20260311t120000z.ndjson

UK Monolith from an already-extracted directory

sct ndjson --rf2 SnomedCT_MonolithRF2_PRODUCTION_20260311T120000Z/

International release with explicit output name

sct ndjson \
  --rf2 SnomedCT_InternationalRF2_PRODUCTION_20250101T120000Z.zip \
  --locale en-US \
  --output snomed-international-20250101.ndjson

Two-release UK edition (clinical + drug extension)

sct ndjson \
  --rf2 SnomedCT_UKClinicalRF2_PRODUCTION_20250401T000001Z.zip \
  --rf2 SnomedCT_UKDrugRF2_PRODUCTION_20250401T000001Z.zip \
  --locale en-GB \
  --output snomed-uk-full-20250401.ndjson

Tip for UK users: prefer the single, pre-merged UK Monolith Edition from NHS TRUD (one --rf2) over hand-layering International + extensions. NHS merges International + UK Clinical + UK Drug + UK Pathology and resolves the conflicts for you; sct recognises it as the "UK Monolith" edition.


Locale and preferred-term selection

--locale chooses the dialect of the preferred term, by selecting the SNOMED language reference set to honour - not just filtering on language code (GB and US English descriptions both have languageCode "en"; only the refset id distinguishes them):

--locale Language reference sets consulted, in priority order
en-GB (default) UK National/Clinical (999001261000000100) → UK dm+d (999000691000001104) → International GB English (900000000000508004)
en-US International US English (900000000000509007)
other en-* GB English → US English

A concept's preferred term is the synonym marked Preferred in the highest-priority refset that has an entry for it, falling back to any preferred synonym, then the FSN. Refsets absent from your input are simply skipped - so en-GB works correctly on an International-only release (it falls through to GB English). Concretely, 80146002 resolves to Appendicectomy under en-GB and Appendectomy under en-US.

Layering multiple --rf2 sources

When you repeat --rf2, sources are layered in argument order, with the last source winning for each repeated component SCTID or refset member UUID. This covers concepts, descriptions, inferred relationships, language memberships, and the loaded SimpleMap, Simple, Association, Complex Map, Extended Map, and Attribute Value families. Replacement happens before grouping and active-state filtering: a later inactive component retracts the earlier description, relationship, preferred-language membership, map or history projection. Empty map/association targets also retract older contributions, rather than silently retaining them. --include-inactive retains inactive concepts, not retired descriptions or relationships.

Layering does not choose winners by effectiveTime. Within each source, files are read in sorted path order; surviving projection rows retain their last-occurrence order for deterministic preferred-term and hierarchy selection. Loading the same Snapshot twice does not duplicate its components. A single Snapshot is assumed to have unique component/member IDs, as required by RF2, and uses the streaming fast path. There is no Module Dependency Reference Set resolution - for robust extension-on-base layering, use a publisher-merged Edition (e.g. the UK Monolith) instead.


Write to stdout (pipe into another tool)

sct ndjson --rf2 ./SnomedCT_Release/ -o - | jq 'select(.id == "22298006")'

--refsets all requires a named output file because payload and history records use companion NDJSON streams; sct fails rather than silently omitting them from stdout. The payload-refset stream is provenance-bound as described below.


Output format

RF2 input is literal UTF-8 tab-delimited text. Double quotes in description terms and reference-set payloads are preserved, not treated as CSV text qualifiers. Quoted text cannot span physical RF2 records; malformed row widths still fail rather than being silently joined.

One JSON object per line, sorted by concept SCTID. Every line is a standalone JSON object - the file is valid NDJSON. The first line is a provenance record ("_type": "sct_provenance") carrying the source edition, release date, the sct version that built the file, and a manifest of any required companion streams - every line after that is a concept record. Older (pre-provenance) NDJSON files without this header line still work; downstream sct commands detect the header by its _type tag and fall through to the concept-record path otherwise.

{
  "id": "22298006",
  "fsn": "Myocardial infarction (disorder)",
  "preferred_term": "Heart attack",
  "synonyms": ["Cardiac infarction", "Infarction of heart", "MI - Myocardial infarction"],
  "hierarchy": "Clinical finding",
  "hierarchy_path": [
    "SNOMED CT Concept",
    "Clinical finding",
    "Disorder of cardiovascular system",
    "Ischemic heart disease",
    "Myocardial infarction"
  ],
  "parents": [{"id": "414795007", "fsn": "Ischemic heart disease (disorder)"}],
  "children_count": 47,
  "active": true,
  "definition_status": "900000000000073002",
  "module": "900000000000207008",
  "effective_time": "20020131",
  "attributes": {
    "finding_site": [{"id": "302509004", "fsn": "Entire heart (body structure)"}],
    "associated_morphology": [{"id": "55641003", "fsn": "Infarct (morphologic abnormality)"}]
  },
  "ctv3_codes": ["X200E"],
  "read2_codes": [],
  "refsets": ["991381000000107"],
  "relationships": [
    {"type_id": "363698007", "destination_id": "302509004", "group": 0},
    {"type_id": "116676008", "destination_id": "55641003", "group": 0}
  ],
  "crossmaps": [
    {
      "system": "icd10",
      "code": "I219",
      "refset": "999002271000000101",
      "group": 1,
      "priority": 1,
      "advice": "ALWAYS I21.9"
    }
  ],
  "schema_version": 6
}

Fields

Field Type Description
id string SNOMED CT concept identifier (SCTID)
fsn string Fully Specified Name - unique, includes semantic tag in parentheses
preferred_term string Preferred synonym for the requested locale
synonyms string[] All other active synonyms (preferred term excluded)
hierarchy string Top-level hierarchy label (e.g. Clinical finding, Procedure)
hierarchy_path string[] Ancestor chain from root to this concept (semantic tags stripped)
parents {id, fsn}[] Direct IS-A parents, sorted by SCTID
children_count integer Number of direct IS-A children in this release
active boolean Always true unless --include-inactive is used
definition_status string RF2 definition-status SCTID: primitive or fully defined (schema v6)
module string SNOMED module identifier
effective_time string Date this concept last changed, YYYYMMDD
attributes object Named attribute groups with {id, fsn}[] values
ctv3_codes string[] CTV3 crossmap codes (UK edition only; empty array otherwise)
read2_codes string[] Read v2 codes (UK edition only; empty array otherwise)
refsets string[] SCTIDs of reference sets this concept belongs to (populated with --refsets simple)
relationships {type_id, destination_id, group}[] Typed attribute relationships - SCTID-keyed, with group number (schema v4). The SCTID-preserving counterpart of attributes; consumed by ECL attribute refinement
crossmaps object[] SNOMED CT → external map targets from RF2 ExtendedMap refsets (schema v5; populated with --refsets all)
schema_version integer Artefact schema version (currently 6)

Payload-refset companion stream

With --refsets all, sct writes <stem>.refsets.ndjson and declares it in the main provenance header with its schema version, record count, and content fingerprint. The companion's first line is an sct_refset_provenance header containing both a fingerprint of its records and the provenance/fingerprint of the concept NDJSON it belongs to. Remaining lines are typed sct_complex_map_refset_member, sct_extended_map_refset_member, or sct_attribute_value_refset_member records. Each record retains the full RF2 member envelope as canonical snake-case fields (id, effective_time, active, module_id, refset_id, and referenced_component_id) plus every family payload field. This keeps inactive members, null maps, unknown map systems, and rows that reference descriptions or relationships out of the concept-only refsets array without losing them.

sct sqlite verifies both fingerprints and the release identity before loading the companion stream. A stale, mismatched, or modified sidecar fails the rebuild transaction rather than producing a mixed-release database.

The main and companion streams are fully written and synced to same-directory temporary files before publication. Companions switch first and the manifest-bearing main stream switches last; if an ordinary filesystem replacement fails, already-switched files are rolled back to the previous bundle.

Artefact properties

  • One line per active concept (inactive omitted unless --include-inactive)
  • Stable ordering by concept ID
  • fsn, preferred_term, and synonyms are unbounded-length strings - no truncation at 255 or any other length, so SNOMED International's July 2026 increase of the maximum description length to 4096 characters (for long multivalent-vaccine ingredient lists) needs no schema change here
  • Locale-aware preferred terms
  • Self-contained: each line is independently interpretable
  • Greppable: grep "22298006" snomed.ndjson

Querying with standard tools

The artefact is designed to be queried with jq without any custom tooling.

# Look up a concept by SCTID
jq 'select(.id == "22298006")' snomed.ndjson

# Search by preferred term (case-insensitive)
jq 'select(.preferred_term | test("myocardial infarction"; "i"))' snomed.ndjson \
  | head -1 | jq '{id, preferred_term, hierarchy}'

# Count concepts by top-level hierarchy
jq -r '.hierarchy' snomed.ndjson | sort | uniq -c | sort -rn | head -10

# Find concepts with a specific attribute
jq 'select(.attributes.finding_site != null) | {id, preferred_term}' snomed.ndjson

# All concepts with CTV3 mappings
jq 'select(.ctv3_codes | length > 0) | {id, preferred_term, ctv3_codes}' snomed.ndjson

# Concepts modified in a specific release
jq 'select(.effective_time == "20260301") | .preferred_term' snomed.ndjson

Which TRUD download to use

TRUD item Use it? Notes
Monolith Edition, RF2: Snapshot ✅ Recommended International + UK clinical + dm+d in one directory. Single --rf2 argument.
Clinical Edition, RF2: Full, Snapshot & Delta ✅ Works Snapshot files are used; Full and Delta ignored.
Drug Extension, RF2: Full, Snapshot & Delta ⚠️ Supplement Use as a second --rf2 alongside Clinical Edition.
Clinical Edition, RF2: Delta ❌ Won't work No Snapshot files.
Cross-map Historical Files ❌ Not needed Ignored by sct.

Determinism

Given the same RF2 Snapshot directory and --locale, the concept records in sct ndjson's output - every line after the first - are always byte-for-byte identical. The first line is the provenance header (see above), which embeds a created_at build timestamp, so it differs between runs even against identical input. Exclude it when checksumming for reproducibility:

tail -n +2 snomed-uk-20260311.ndjson | sha256sum

The concept lines can be checksummed this way, committed to git-lfs, and used as a pinned dependency.


RF2 file patterns recognised

sct scans the supplied directory recursively for:

Pattern Content
sct2_Concept_Snapshot_*.txt Concept identifiers and status
sct2_Description_Snapshot_*.txt Terms and synonyms
sct2_Relationship_Snapshot_*.txt IS-A and attribute relationships (inferred)
der2_cRefset_Language_*.txt Language reference sets (preferred term acceptability)
der2_sRefset_SimpleMap_*.txt Simple map reference sets (CTV3/Read v2 crossmaps)
der2_Refset_Simple_*.txt Generic concept-level Simple reference sets (membership only, e.g. SCR exclusion); loaded with --refsets simple (default) or all
der2_*Refset_ExtendedMap_*.txt ExtendedMap reference sets (SNOMED CT → ICD-10 / OPCS-4); loaded with --refsets all only
der2_*Refset_ComplexMap*Snapshot*.txt ComplexMap reference sets; payload preserved verbatim with --refsets all without guessing a target system
der2_cRefset_AttributeValue*Snapshot*.txt AttributeValue reference sets, including concept inactivation indicators; loaded with --refsets all only
der2_cRefset_Association_*.txt Historical Association reference sets (inactive-concept forwarding); loaded with --refsets all only

Stated relationship files (sct2_StatedRelationship_*) are intentionally skipped - the inferred release is used for hierarchy and attributes. Full and Delta files are ignored.


Next: load into SQLite with sct sqlite, export to Parquet with sct parquet, or generate embeddings with sct embed.