Create an Ontology
An ontology defines the structured data you want to extract from a specific kind of document, together with the label hierarchy used to classify it. This guide walks you through designing, writing, registering, and testing an ontology from scratch.
An ontology defines the structured data you want to extract from a specific kind of document, together with the label hierarchy used to classify it. This guide walks you through designing, writing, registering, and testing an ontology from scratch.
An ontology has two parts:
- Taxonomy — a hierarchy of labels used for document classification (e.g.,
invoice→utility_invoice). - Fields — the structured data to extract, attached at each level of the taxonomy.
Extraction plugins use ontologies to know what data to extract and where to store it.
1. Design Your Schema
Start by identifying the document type you are targeting and listing every field you need to extract. Because the taxonomy is hierarchical, also decide which sub-types exist and which fields are specific to them.
Consider these questions:
- What top-level document type does this ontology model? (This becomes the root taxonomy
label.) - What sub-categories exist beneath it? (These become entries under
children.) - What fields are always present? (Mark these
required: true.) - What fields are sometimes present? (Leave these
required: false.) - Which fields are specific to a sub-type versus inherited from the parent?
Our example:
invoice
- invoice_total (required)
- invoice_date (required)
- vendor_name (required)
- utility_invoice (sub-type)
- utility_type (required)2. Write the Ontology YAML
Ontologies are defined in YAML. The taxonomy is a tree: a root label with its own fields, and optional children, each of which can add fields of its own. Child labels inherit their parent's fields.
"color:#5c6370;font-style:italic"># Metadata
"color:#e06c75">name: "Invoice Ontology"
"color:#e06c75">description: "Schema for invoice document classification and data extraction"
"color:#e06c75">version: "1.0.0"
"color:#5c6370;font-style:italic"># Taxonomy — hierarchical labels
"color:#e06c75">taxonomy:
"color:#e06c75">label: "invoice"
"color:#e06c75">description: "General invoice document"
"color:#5c6370;font-style:italic"># Fields for invoice documents
"color:#e06c75">fields:
- name: "invoice_total"
"color:#e06c75">description: "Total amount on the invoice"
"color:#e06c75">dtype: "number"
"color:#e06c75">required: "color:#d19a66">true
- name: "invoice_date"
"color:#e06c75">description: "Date the invoice was issued (YYYY-MM-DD)"
"color:#e06c75">dtype: "date"
"color:#e06c75">required: "color:#d19a66">true
- name: "invoice_number"
"color:#e06c75">description: "Invoice number or ID"
"color:#e06c75">dtype: "string"
"color:#e06c75">required: "color:#d19a66">false
- name: "vendor_name"
"color:#e06c75">description: "Name of the vendor or service provider"
"color:#e06c75">dtype: "string"
"color:#e06c75">required: "color:#d19a66">true
- name: "vendor_address"
"color:#e06c75">description: "Vendor's address"
"color:#e06c75">dtype: "string"
"color:#e06c75">required: "color:#d19a66">false
- name: "due_date"
"color:#e06c75">description: "Payment due date (YYYY-MM-DD)"
"color:#e06c75">dtype: "date"
"color:#e06c75">required: "color:#d19a66">false
- name: "line_items"
"color:#e06c75">description: "Individual line items on the invoice"
"color:#e06c75">dtype: "array"
"color:#e06c75">required: "color:#d19a66">false
"color:#5c6370;font-style:italic"># Child labels (sub-types of invoice)
"color:#e06c75">children:
- label: "utility_invoice"
"color:#e06c75">description: "Invoice for utility services (electricity, water, gas)"
"color:#5c6370;font-style:italic"># Additional fields specific to utility invoices
"color:#e06c75">fields:
- name: "utility_type"
"color:#e06c75">description: "Type of utility (electricity, water, gas, internet)"
"color:#e06c75">dtype: "string"
"color:#e06c75">required: "color:#d19a66">true
- name: "account_number"
"color:#e06c75">description: "Utility account number"
"color:#e06c75">dtype: "string"
"color:#e06c75">required: "color:#d19a66">false
- name: "usage_amount"
"color:#e06c75">description: "Amount of utility consumed (kWh, gallons, etc.)"
"color:#e06c75">dtype: "number"
"color:#e06c75">required: "color:#d19a66">false
- label: "service_invoice"
"color:#e06c75">description: "Invoice for professional services"
"color:#e06c75">fields:
- name: "service_type"
"color:#e06c75">description: "Type of service provided"
"color:#e06c75">dtype: "string"
"color:#e06c75">required: "color:#d19a66">true
- name: "hourly_rate"
"color:#e06c75">description: "Hourly rate for service"
"color:#e06c75">dtype: "number"
"color:#e06c75">required: "color:#d19a66">false3. Validate with the SDK
Validate your ontology locally before registering it:
pip install bizsupply-sdk
bizsupply init ontology --name my_ontology ="color:#5c6370;font-style:italic"># Scaffold a template
bizsupply validate invoice_ontology.yaml ="color:#5c6370;font-style:italic"># Validate the schemaCheck these rules:
- Field types: each
dtypemust be one ofstring,number,date,boolean,array. - Label names: use snake_case (e.g.,
utility_invoice, notUtility Invoice). - Hierarchy: parent fields are inherited by children.
- Required fields: mark critical fields with
required: true.
Flattened labels:
The platform flattens hierarchical labels so documents can be queried by parent or child:
invoice→["invoice"]utility_invoice→["invoice", "utility_invoice"]
4. Register the Ontology
Register the ontology by uploading the YAML file as multipart form data:
POST /api/v1/ontologies
Authorization: Bearer <your-jwt-token>
Content-Type: multipart/form-data
Fields:
- ontology_file: invoice_ontology.yamlUsing curl:
curl -X POST "https://api.bizsupply.ai/api/v1/ontologies" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-F "ontology_file=@invoice_ontology.yaml"Response:
{
"ontology_id": "01HQZX3K4M2N5P7Q8R9S0T1U2V",
"name": "Invoice Ontology",
"message": "Ontology registered successfully"
}Save the ontology_id — you'll need it when creating pipelines.
5. Verify Registration
GET /api/v1/ontologies
Authorization: Bearer <your-jwt-token>{
"count": 1,
"ontologies": [
{
"ontology_id": "01HQZX3K4M2N5P7Q8R9S0T1U2V",
"name": "Invoice Ontology",
"description": "Schema for invoice document classification and data extraction",
"version": "1.0.0",
"taxonomy": {
"label": "invoice",
"fields": [
{ "name": "invoice_total", "dtype": "number", "required": true }
],
"children": [
{ "label": "utility_invoice", "fields": [] }
]
}
}
]
}6. Create a Matching Extraction Plugin
Your Extraction plugin receives the ontology fields the platform resolves from the document's labels, and uses them to build the LLM prompt. Extraction is asynchronous, reads the raw file bytes (file_data), and returns an ExtractionResult.
from bizsupply_sdk import ExtractionPlugin, ExtractionResult, ConfigurableParameter
class InvoiceExtractorPlugin(ExtractionPlugin):
"""Extracts invoice data using ontology field definitions.
The platform resolves fields from the ontology based on the document's
labels and injects them as the `fields` parameter.
"""
configurable_parameters = [
ConfigurableParameter(
parameter_name="extraction_prompt_id",
parameter_type="str",
default_value=None,
description="Prompt ID for extraction",
),
]
async def extract(self, document, file_data, mime_type, fields, configs) -> ExtractionResult:
"""Extract structured data from a single document.
Args:
document: The classified document.
file_data: Raw file bytes (pre-fetched by the platform).
mime_type: MIME type of the file.
fields: Ontology fields resolved from document.labels.
configs: Runtime configuration.
Returns:
An ExtractionResult with the extracted data.
"""
fields_json = self.format_fields_for_prompt(fields)
prompt = f"""Extract the following invoice data from this document.
Fields to extract:
{fields_json}
Return a JSON object with the field names as keys and the extracted values."""
# The LLM reads the attached file bytes directly.
result = await self.prompt_llm(
prompt=prompt,
file_data=file_data,
mime_type=mime_type,
)
if not result or not isinstance(result, dict):
return ExtractionResult(data={})
return ExtractionResult(
data=result,
llm_fields=list(result.keys()),
)prompt_llm(...) returns the parsed response (dict | list | None); it accepts file_data, mime_type, an optional schema, and an optional model_name. The platform handles persistence once you return the ExtractionResult.
7. Wire It Into a Pipeline and Execute
Reference the ontology ID (and your source, classifier, and extractor plugins) when creating a pipeline:
POST /api/v1/pipelines
Authorization: Bearer <your-jwt-token>
Content-Type: application/json
{
"name": "Invoice Processing Pipeline",
"description": "Classify and extract invoice data",
"plugin_ids": [
"YOUR_SOURCE_PLUGIN_ID",
"YOUR_CLASSIFIER_PLUGIN_ID",
"YOUR_EXTRACTOR_PLUGIN_ID"
],
"ontology_catalogs_ids": [
"01HQZX3K4M2N5P7Q8R9S0T1U2V"
]
}Then start a job from the pipeline and inspect the extracted fields:
="color:#5c6370;font-style:italic"># Execute
POST /api/v1/jobs/from-pipeline
Authorization: Bearer <your-jwt-token>
Content-Type: application/json
{ "pipeline_id": "01HQZY5M7N8P9Q0R1S2T3U4V5W" }="color:#5c6370;font-style:italic"># Check results
GET /api/v1/documents?labels=invoice
Authorization: Bearer <your-jwt-token>{
"total_count": 5,
"documents": [
{
"id": "01HR001A2B3C4D5E6F7G8H9J0K",
"labels": ["invoice"],
"data": {
"invoice_total": 1500.00,
"invoice_date": "2025-10-15",
"invoice_number": "INV-12345",
"vendor_name": "Acme Corp",
"due_date": "2025-11-15"
}
}
],
"next_cursor": null
}See Create a Pipeline for the full pipeline workflow.
8. Iterate on Field Descriptions
The quality of extracted data depends heavily on field descriptions — they are included directly in the LLM prompt. If the LLM extracts incorrect values, make the description more specific about where to find the value and how to format it. A clear description like "The purchase order number, typically formatted as PO-YYYY-NNNNN" produces better results than a vague one like "PO number".
9. Update the Ontology
To add or modify fields, re-upload the YAML file:
PUT /api/v1/ontologies/{ontology_id}
Authorization: Bearer <your-jwt-token>
Content-Type: multipart/form-data
Fields:
- ontology_file: updated_invoice_ontology.yamlThe version auto-increments on update, and pipelines that reference the ontology pick up the new version automatically.
Field Type Reference
Each field's dtype is one of:
| dtype | Description | Example |
|---|---|---|
string | Text value | "Acme Corp" |
number | Numeric value (integer or decimal) | 1500.00 |
date | Date value, normalized to YYYY-MM-DD | "2025-10-15" |
boolean | True/false value | true |
array | List of values or objects | ["item1", "item2"] |
LLM-Powered Ontology Generation (Ontology Fabric)
Instead of writing YAML manually, you can describe your ontology in natural language and let the platform generate it using an LLM.
How It Works
User prompt (natural language)
|
v
POST /api/v1/ontologies/preview
|-- LLM generates the taxonomy
|-- SDK validates the structure
|-- Returns an editable taxonomy (no persistence)
v
User edits the taxonomy (optional)
|
v
POST /api/v1/ontologies/generate
|-- SDK validates the edited taxonomy
|-- Scoring (structural + overlap + LLM quality)
|-- Score >= 7.0 -> Auto-approved, persisted immediately
|-- Score < 7.0 -> Sent for human review
v
A platform administrator reviews (if needed)
|-- GET /api/v1/ontologies/reviews/pending
|-- POST .../approve -> Ontology persisted
|-- POST .../reject -> Ontology discarded with a reasonStep 1: Preview (No Commitment)
Describe what you need in plain English:
curl -X POST "https://api.bizsupply.ai/api/v1/ontologies/preview" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"prompt": "Create an ontology for classifying energy supply contracts with fields for pricing, consumption periods, and renewal dates"}'The response includes the generated taxonomy as an editable tree, plus the label hierarchy chain and the field list. You can modify the taxonomy before submitting it.
Step 2: Generate (Validate + Score + Persist)
Submit the (optionally edited) taxonomy for validation and scoring:
curl -X POST "https://api.bizsupply.ai/api/v1/ontologies/generate" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"taxonomy": {"label": "contract", "children": [...]},
"scope": "tenant",
"name": "Energy Contract Ontology",
"description": "Schema for energy supply contracts"
}'The response review_status field indicates the outcome:
APPROVED— the score was 7.0 or higher. The ontology is persisted and ready to use.HUMAN_REVIEW— the score was below 7.0. A platform administrator must approve it before it becomes available.
Credit Gating
Both preview and generate check the tenant's credit balance before calling the LLM. If the tenant has no remaining credits, the API returns 402 Payment Required with an InsufficientCreditsError.
Scoring Dimensions
The quality score (0–10) evaluates four dimensions:
| Dimension | What It Checks |
|---|---|
| Completeness | Required fields present, descriptions populated, hierarchy depth |
| Field quality | Valid types, meaningful names, required/optional balance |
| Naming conventions | snake_case labels, consistent naming patterns |
| Hierarchy depth | Appropriate nesting (not too flat, not too deep) |
Overlap Detection
Before persisting, the platform checks for overlap with existing ontologies in the same tenant:
- It computes Jaccard similarity on the label sets of the new and existing ontologies.
- If overlap exceeds 50%, the quality score is capped at 5.0, forcing human review.
- The review
observationsfield explains the overlap finding.
Human Review
Ontologies scoring below 7.0 enter the review queue. Platform administrators can:
- List pending reviews:
GET /api/v1/ontologies/reviews/pending - Approve:
POST /api/v1/ontologies/reviews/{review_id}/approve— persists the ontology - Reject:
POST /api/v1/ontologies/reviews/{review_id}/reject— discards it with a reason
Common Issues
| Issue | Cause | Resolution |
|---|---|---|
Invalid field type: 'decimal' | Unsupported dtype. | Use number. Supported: string, number, date, boolean, array. |
| Child labels not inheriting parent fields | Children aren't nested under children:. | Ensure each sub-type is listed under the parent's children: block. |
| Fields not extracted | Extraction didn't resolve the ontology fields, or classification didn't run. | Confirm the document has the expected labels and that the ontology ID is in the pipeline's ontology_catalogs_ids. |
| Required field returns null | The field is absent, or the LLM missed it. | Improve the field description; consider making it optional if it is legitimately absent on some documents. |
| Ontology not found in pipeline | Wrong or foreign ontology_id. | Verify with GET /api/v1/ontologies/{ontology_id} that it exists and belongs to your tenant. |
Best Practices
- Write precise field descriptions — the description is the single biggest factor in extraction quality. Be specific about location, format, and common variations.
- Start with required fields only — add optional fields after you confirm the required ones extract correctly.
- Use hierarchies — organize labels from general to specific (
invoice→utility_invoice). - Consistent naming — use snake_case for all labels and field names.
- Version carefully — update the ontology by re-uploading; the version auto-increments and pipelines pick up the change. Test on a small batch before relying on it in production.
- Test with real documents — synthetic samples rarely capture real-world variability. Validate against at least 10 real documents.
Next Steps
- Create an extraction plugin: follow Create a Plugin
- Build a complete pipeline: follow Create a Pipeline
- Author reusable prompts: follow Create a Prompt