Skip to content

Ontology Term Labels from OBO Files and OLS4

Annotators search the Cell Ontology, HPO, GO, or any OBO ontology, and each pick is saved with its term id, name, IRI, and release, offline or through OLS4.

New in v2.13.0

A label_source block on a radio, multiselect, span, or image_annotation scheme adds a search box over an ontology. Annotators type a name, a synonym, or an id and pick a term instead of scrolling a fixed label list. Each pick is stored with the term's id, name, IRI, and the ontology release the term came from. Biocurators use the feature for cell type annotation with the Cell Ontology, phenotype annotation with the Human Phenotype Ontology (HPO), and disease normalization with MONDO, and the tagging of chemical mentions with ChEBI terms, tissue regions on a slide with UBERON terms, and gene products with GO terms. Potato's agreement metrics and the export read the term id. An annotator who searched "B cell" and an annotator who typed the id CL:0000236 agree because both answers store the same id.

The OBO flat file format is the plain-text release format of the OBO Foundry ontologies. Potato reads OBO 1.4. The Ontology Lookup Service (OLS4) is EMBL-EBI's search service and API for biomedical ontologies, and an organization can also run an OLS4 server of its own.

  • Local file: with service: file, Potato searches an OBO 1.4 file inside the task directory. A file source works offline.
  • OLS4: with service: ols4, Potato searches an OLS4 server. Every search sends the typed text to that server, and Potato refuses to start unless the scheme sets allow_network: true. The Potato server makes the request, and the annotator's browser only talks to Potato.
  • Stored record: each pick is saved as {obo_id, label, iri, ontology_version}. Potato's agreement metrics compare the obo_id.
  • Export: python -m potato.export with --format ontology_terms writes one row per picked term.
  • No extra install: Potato parses OBO with its own reader, and OLS4 calls use the requests package Potato already depends on.

Configuration

The config below sets up three schemes over the synthetic sample ontology from Potato's example. The first scheme asks for one main cell type through the search box alone, the second scheme offers a fixed None option above the search box, and the third scheme tags text spans with immune cell terms only:

yaml
annotation_task_name: "Cell types from an ontology"
task_dir: .
output_annotation_dir: annotation_output
data_files: [data/cell-descriptions.jsonl]
item_properties: {id_key: id, text_key: text}
 
user_config:
  allow_all_users: true
 
annotation_schemes:
  - annotation_type: radio
    name: main_cell_type
    description: "Which cell type does the description mainly talk about?"
    labels: []                  # search box only
    label_requirement: {required: true}
    label_source:
      ontology: scl
      service: file
      file: ontologies/scl-sample.obo
 
  - annotation_type: multiselect
    name: other_cell_types
    description: "Which other cell types are mentioned?"
    labels: [None]              # a fixed option above the search box
    label_source:
      ontology: scl
      service: file
      file: ontologies/scl-sample.obo
 
  - annotation_type: span
    name: immune_cell_mentions
    description: "Highlight each immune cell mention and tag its type"
    labels: []
    label_source:
      ontology: scl
      service: file
      file: ontologies/scl-sample.obo
      root: SCL:0000001         # immune cell and its is_a descendants

With labels: [], or with no labels key, the search box is the only way to answer. With a list, the fixed labels show as usual and the search box sits below the fixed labels. A fixed label is stored as a plain label name, and only a term picked from the search is stored as an ontology term. Because the first scheme sets required, Potato keeps Next blocked until the annotator picks a term. For the third scheme, Potato limits the search to the root immune cell term and every term below that term by is_a. A neuron term never appears in the span search.

Potato ships no ontology files. Download the .obo release you need into the task directory and keep the file with the project. The HPO release is http://purl.obolibrary.org/obo/hp.obo, and the Cell Ontology release is http://purl.obolibrary.org/obo/cl.obo. The example's scl-sample.obo is a made-up ontology with 19 current terms, one obsolete term, and the prefix SCL. The SCL ids are not Cell Ontology ids.

For an image annotation scheme, the same block goes beside the drawing tools. The partial block below adds a fourth scheme to the config above:

yaml
  - annotation_type: image_annotation
    name: cells_in_image
    description: "Box each cell and name its type"
    tools: [bbox, polygon]
    labels: []
    label_source:
      ontology: scl
      service: file
      file: ontologies/scl-sample.obo

Searching OLS4 instead of a file

The partial block below replaces annotation_schemes in the config above with one scheme that searches the Cell Ontology on EBI's OLS4 server. The ontology value is the OLS4 ontology id, such as cl, hp, go, mondo, chebi, or uberon:

yaml
annotation_schemes:
  - annotation_type: radio
    name: cell_type
    description: "Which cell type is described?"
    labels: []
    label_source:
      ontology: cl
      service: ols4
      allow_network: true       # required for ols4
      root: CL:0000000          # optional, sent to OLS4 as childrenOf
      timeout_s: 5

Set base_url to search a self-hosted OLS4 server. The allow_network key is a deliberate opt-in. A study whose search text must stay on your network uses service: file. Potato refuses to start when an OLS4 block lacks allow_network: true.

Options

Every key label_source accepts:

KeyServiceDefaultMeaning
ontologybothrequiredA short ontology name (cl, hp). The name is shown in the search box label, and for OLS4 the lowercased name is the ontology id sent to the server
servicebothrequiredfile or ols4
filefilerequiredPath to an OBO 1.4 file, relative to task_dir and inside task_dir. A URL is refused because Potato does not download ontology files
max_file_mbfile300A file larger than the given size in megabytes is refused at startup
rootbothnoneA term id such as CL:0000000. Only that term and the terms below the term by is_a are offered
max_resultsboth20Matches shown per search, an integer from 1 to 100
allow_networkols4falseMust be true for service: ols4
base_urlols4https://www.ebi.ac.uk/ols4/apiThe OLS4 API root
timeout_sols45Seconds to wait for OLS4, from 0.5 to 30

The server and python -m potato.validate_cli refuse a config and name the offending key when any of the following conditions holds:

  • label_source sits on a scheme type other than radio, multiselect, span, or image_annotation;
  • service is not file or ols4, or a key belongs to the other service (base_url with file);
  • a key is unknown;
  • the file is missing, outside task_dir, larger than max_file_mb, an OWL file, or has no terms;
  • root is not a term in the file, or is marked obsolete; or
  • service: ols4 is set without allow_network: true.

OBO file parsing

Potato reads the data-version, ontology, and default-namespace header tags. In each [Term] stanza, the reader takes id, name, namespace, synonym, def, is_a, alt_id, and is_obsolete, plus the OBO 1.0 synonym tags such as exact_synonym found in older files. [Typedef] stanzas are skipped, and relationship: and intersection_of: lines are not edges. Potato therefore builds a root subtree from is_a alone. For example, a UBERON subtree has the subclasses of the root and none of the structures linked to the root only by part_of.

The stored ontology_version is the file's data-version header, such as scl/releases/2026-10-01 in the sample. A file without the header gets sha256: plus the first 12 hex digits of the file's hash. The file is parsed once and kept in memory. Potato parses the file again when the file's size or modification time changes. A new release copied over the old file takes effect on the next search without a restart.

Obsolete terms are left out of search and out of every subtree, and Potato does not follow a replaced_by tag to the newer term. A search for an alt_id finds the primary term, and Potato stores the primary id. OWL files are refused with a message. Most OBO Foundry ontologies publish an .obo release beside the .owl file. Download the .obo release, or convert an OWL file with ROBOT:

bash
robot convert --input cl.owl --output cl.obo

For a large ontology such as NCBITaxon, use a slim or subset file, raise max_file_mb, or search the ontology through OLS4.

Searching and picking terms

Annotators type at least two characters of a name, a synonym, or a term id. A file search ranks a term id first (CL:0000236, CL_0000236, an alt_id, or the full PURL), then an exact name, a name that starts with the typed text, and a name whose words start with each typed word. Synonyms are matched the same three ways, followed by text found anywhere in a name and then anywhere in a synonym. Every synonym scope counts (EXACT, BROAD, NARROW, and RELATED), and a match found through a synonym shows the synonym under the term name. A result also shows the term id and the first 240 characters of the definition. In the sample ontology, typing white blood finds leukocyte through the synonym "white blood cell", and typing SCL:0000099 finds T cell through the term's alt_id.

When more terms match than max_results, a status line gives the number shown and the total, and asks the annotator to type more. A line under the box names the source. The line shows the ontology and the file's version for a file source, and the ontology name followed by "via OLS4" for an OLS4 source.

  • Keyboard: Up and Down move through the matches, Enter picks the highlighted match, Escape closes the list, and a second Escape clears the box. Clicking or tapping a match also picks the match, and each match is a tap target at least 44 pixels tall.
  • radio: the picked term appears as a selected option, and picking another term replaces the earlier term.
  • multiselect: each picked term appears as a ticked checkbox, and unticking the checkbox removes the term.
  • span: the picked term becomes the active span label, and the annotator then highlights the text. The chip above the highlight shows the term name.
  • image_annotation: the picked term becomes a label button and the active label, and the annotator then draws. Terms already used on an image appear as buttons when the page loads.

Pocket Mode shows no search box. A task with a label_source scheme therefore always uses the regular annotation page. The regular page also works on a phone.

Network access and OLS4 caching

Potato makes no network request for a service: file scheme at any point. Once the .obo file is in the task directory, a file source runs in an air-gapped deployment. Searches run against the parsed file on the Potato server. An OLS4 scheme without allow_network: true cannot reach the network because the server refuses to start with that config and suggests a local file instead.

With service: ols4 and allow_network: true, the Potato server sends each search to OLS4 with obsoletes=false. OLS4 then leaves obsolete terms out of the results. Search answers are cached in the server's memory for six hours, up to 512 distinct searches, and the ontology's release string is cached for six hours as well. The browser may reuse an answer for five minutes. Every term OLS4 returns is also written to <output_annotation_dir>/ontology_cache/<ontology>-ols4-terms.json. After a reload, Potato reads the term names on span chips and image label buttons from that file, and the export reads the same file.

When OLS4 is slow or unreachable, the search box shows "Search failed: OLS4 did not answer within 5 s." or a message starting "Search failed: OLS4 request failed", and annotators can still answer the fixed labels. Searches answered in the previous six hours are served from the cache. Terms already picked stay saved because a radio or multiselect answer holds the whole record and span and image answers resolve through the term file on disk. If Potato cannot read the ontology's release from OLS4, picks are stored with an empty ontology_version until the next release lookup, up to six hours later. For a study that must run through network outages, download the .obo file and switch to service: file.

Stored answer

A picked term is stored as the record below:

json
{"obo_id": "SCL:0000005", "label": "CD4-positive T cell",
 "iri": "http://purl.obolibrary.org/obo/SCL_0000005",
 "ontology_version": "scl/releases/2026-10-01"}

For a file source, the iri is the OBO PURL built from the id, and for OLS4 the iri is the IRI that OLS4 returns. A radio or multiselect answer keeps the ontology_version from the moment of the pick. After the .obo file is replaced, the answer still names the release the annotator searched.

SchemeStored as
radio, multiselectOne label per term. The label name is the obo_id, and the value is the record above as a JSON string
spanThe span's label (name) is the obo_id
image_annotationEach shape's label is the obo_id, inside the usual _data JSON

Spans and shapes store the id alone. The export looks up the name, IRI, and version in the .obo file for service: file, and in the OLS4 term file under ontology_cache for service: ols4. For spans and shapes in file mode, the export writes the version of the .obo file at export time.

Export

The ontology_terms format writes ontology_terms.csv and ontology_terms.jsonl, one row per picked term:

bash
python -m potato.export --config config.yaml --format ontology_terms --output out/

For one annotator on the first item of the config above, ontology_terms.csv reads:

text
user_id,instance_id,schema,annotation_type,ontology,obo_id,label,iri,ontology_version,start,end,text,shape_index,shape_type,known
ana,cells_01,main_cell_type,radio,scl,SCL:0000005,CD4-positive T cell,http://purl.obolibrary.org/obo/SCL_0000005,scl/releases/2026-10-01,,,,,,true
ana,cells_01,other_cell_types,multiselect,scl,SCL:0000011,macrophage,http://purl.obolibrary.org/obo/SCL_0000011,scl/releases/2026-10-01,,,,,,true
ana,cells_01,immune_cell_mentions,span,scl,SCL:0000011,macrophage,http://purl.obolibrary.org/obo/SCL_0000011,scl/releases/2026-10-01,128,150,large phagocytic cells,,,true

The start, end, and text columns are filled for spans, and shape_index and shape_type for image shapes. The known column is false when the id is not in the current file or the OLS4 term file. A term that a newer release removed or made obsolete gets false. The export prints a warning with the count of such rows. Fixed labels on the same scheme are not rows in ontology_terms. The general csv export writes a radio or multiselect term as a column named <schema>.<obo_id> holding the JSON record, and spans and shapes keep the obo_id as their label. See Export Formats for the other formats.

Agreement

Potato's agreement metrics compare the obo_id. A radio scheme is scored as a nominal label (percent agreement, Cohen's kappa, Fleiss' kappa, and Krippendorff's alpha), a multiselect scheme by Jaccard overlap and MASI-weighted alpha over the sets of term ids, a span scheme by span position and obo_id, and an image_annotation scheme by each shape's geometry and term id. Two annotators who pick the same term agree even when their records hold different ontology_version values. A parent and a child term count as different labels, and the agreement code gives no partial credit along the ontology hierarchy. For example, T cell and CD4-positive T cell are scored as a disagreement. For a hierarchy-aware comparison, export ontology_terms and compute the measure from the obo_id column against the same .obo release.

Example

bash
python potato/flask_server.py start examples/classification/ontology-labels/config.yaml -p 8000

The example has four short cell descriptions, the synthetic scl-sample.obo ontology, and the three schemes from the config above.

Further reading

  • Life Sciences - Potato for biocuration and life-science annotation
  • Healthcare - Potato for clinical and biomedical annotation
  • Entity Linking - Linking a mention to a knowledge-base entry, where the span label stays a fixed category
  • Radio and Multiselect - The single-choice and multiple-choice schemes that take label_source
  • Export Formats - Potato's export formats and the export command

For implementation details, see the source documentation.