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 setsallow_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 theobo_id. - Export:
python -m potato.exportwith--format ontology_termswrites one row per picked term. - No extra install: Potato parses OBO with its own reader, and OLS4 calls use the
requestspackage 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:
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 descendantsWith 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:
- 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.oboSearching 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:
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: 5Set 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:
| Key | Service | Default | Meaning |
|---|---|---|---|
ontology | both | required | A 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 |
service | both | required | file or ols4 |
file | file | required | Path 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_mb | file | 300 | A file larger than the given size in megabytes is refused at startup |
root | both | none | A term id such as CL:0000000. Only that term and the terms below the term by is_a are offered |
max_results | both | 20 | Matches shown per search, an integer from 1 to 100 |
allow_network | ols4 | false | Must be true for service: ols4 |
base_url | ols4 | https://www.ebi.ac.uk/ols4/api | The OLS4 API root |
timeout_s | ols4 | 5 | Seconds 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_sourcesits on a scheme type other thanradio,multiselect,span, orimage_annotation;serviceis notfileorols4, or a key belongs to the other service (base_urlwithfile);- a key is unknown;
- the file is missing, outside
task_dir, larger thanmax_file_mb, an OWL file, or has no terms; rootis not a term in the file, or is marked obsolete; orservice: ols4is set withoutallow_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:
robot convert --input cl.owl --output cl.oboFor 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:
{"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.
| Scheme | Stored as |
|---|---|
radio, multiselect | One label per term. The label name is the obo_id, and the value is the record above as a JSON string |
span | The span's label (name) is the obo_id |
image_annotation | Each 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:
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:
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,,,trueThe 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
python potato/flask_server.py start examples/classification/ontology-labels/config.yaml -p 8000The 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.