Create a Classification Plugin
Classification plugins analyze document content and assign one or more category labels. They are the first processing stage in most pipelines — downstream extraction steps depend on accurate classification to select the correct ontology and field definitions.
Classification plugins analyze document content and assign one or more category labels. They are the first processing stage in most pipelines — downstream extraction steps depend on accurate classification to select the correct ontology and field definitions.
What Classification Plugins Do
A classification plugin receives a document and returns a string label that categorizes it. This label determines which extraction ontology the platform applies next. For example, a document classified as "invoice" routes to the invoice ontology, while a "contract" routes to the contract ontology.
- Categorize documents into predefined types (invoice, receipt, contract, purchase order, etc.)
- Support hierarchical classification with multi-level taxonomies
- Use LLM analysis, rule-based logic, or a combination of both
- Return a single string label that maps to a registered ontology taxonomy
Key method: Return a label string from classify() — the platform handles persistence and ontology traversal.
Prerequisites
- Python 3.10+ installed
- bizSupply SDK installed:
pip install bizsupply-sdk - Read the Plugin Interface Specification
- Create a Prompt for classification instructions
- Optionally, create an Ontology with taxonomies matching your classification labels
Step 1 — Write the Plugin Code
Create a new Python file and implement your classification logic by extending ClassificationPlugin. The classify() method is the only required method — it is async, receives the document plus the labels available at the current hierarchy level, and must return a string label (or None).
from bizsupply_sdk import ClassificationPlugin
class InvoiceClassifierPlugin(ClassificationPlugin):
"""
Classifies documents as invoices, receipts, or other.
The platform calls classify() once per level of the ontology tree.
Just pick from available_labels - the platform handles traversal.
"""
# Optional: declare configurable parameters as class attributes
configurable_parameters = [
{
"parameter_name": "classification_prompt_id",
"parameter_type": "str",
"default_value": None,
"description": "Prompt ID for classification (REQUIRED)",
},
]
async def classify(
self,
document,
file_data,
mime_type,
available_labels,
current_path,
configs,
):
"""
Classify at a single hierarchy level.
Args:
document: The document being classified
file_data: Raw file bytes (the LLM reads PDFs directly)
mime_type: File MIME type
available_labels: Ontology labels at this level
current_path: Labels already selected (e.g., ["Energy", "Contract"])
configs: Runtime configuration (prompt IDs, etc.)
Returns:
- Label from available_labels: the platform continues to the next level
- Label NOT in available_labels: tracked as an LLM suggestion
- None: the platform triggers the suggestion workflow
"""
self.logger.info(
f"Classifying {document.document_id} at level {len(current_path)}: "
f"path={current_path}, options={available_labels}"
)
path_str = " > ".join(current_path) if current_path else "Root"
options_str = ", ".join(available_labels)
prompt = f"""You are a document classification expert.
Current classification path: {path_str}
Available categories: {options_str}
Analyze the document and select the most appropriate category from the list above.
Return JSON: {{"category": "selected_category"}}
If none of the categories fit, return: {{"category": null}}
[See attached document file]"""
# Call the LLM (await is REQUIRED — prompt_llm is async)
result = await self.prompt_llm(
prompt=prompt,
file_data=file_data,
mime_type=mime_type,
)
if not result:
self.logger.error(f"Empty LLM response for {document.document_id}")
return None
selected_label = result.get("category")
if not selected_label or selected_label == "null":
self.logger.info(f"LLM returned no category for {document.document_id}")
return None # the platform will trigger the suggestion workflow
self.logger.info(f"Selected '{selected_label}' for {document.document_id}")
return selected_labelLoad prompts with await self.get_prompt(prompt_id) instead of hardcoding them in your plugin. This lets you update prompts without resubmitting code.
Step 2 — Create a Classification Prompt
Register a reusable prompt template with the platform, then reference it from your plugin by prompt_id. This keeps prompt engineering separate from plugin code. Prompts are registered by multipart upload:
curl -X POST "https://api.bizsupply.ai/api/v1/prompts" \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "name=Invoice Classification Prompt" \
-F "prompt=@classification_prompt.txt"Save the returned prompt_id (a UUID) — you pass it to the plugin through the classification_prompt_id configurable parameter when wiring up a pipeline. See Create a Prompt for the full request and options.
Step 3 — Validate and Register
Validate your plugin locally, then submit it for review.
="color:#5c6370;font-style:italic"># Validate the plugin structure
bizsupply validate invoice_classifier.py
="color:#5c6370;font-style:italic"># Submit (multipart — type and configurable parameters are auto-extracted)
curl -X POST "https://api.bizsupply.ai/api/v1/plugins" \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "name=Invoice Classifier" \
-F "description=Classifies documents as invoices, receipts, or other" \
-F "code=@invoice_classifier.py"Review workflow
Plugin code runs inside an isolated execution environment, so every submission — including from platform administrators — is held in a review queue until approved by a platform administrator. The response tells you where your submission landed:
{"review_id": "...", "status": "pending_review"}Your plugin is not available for use until a platform administrator approves the review via POST /api/v1/reviews/{review_id}/approve. Once approved, the plugin keeps the same ID that survived the submit → approve roundtrip and is immediately loadable by pipelines. There is no bypass — this applies to all plugin types. See Create a Plugin for the full cross-type review flow.
Key Methods
| Method | Purpose | ||
|---|---|---|---|
await self.prompt_llm(prompt, file_data, mime_type) | Call the LLM for classification (returns parsed `dict \ | list \ | None`) |
await self.get_prompt(prompt_id) | Load a registered prompt template by ID | ||
self.logger | Plugin-specific logger (self.logger.info(...), .warning(...), .error(...)) |
The platform handles document content fetching, ontology traversal, and classification persistence automatically.
Hierarchical Classification
For complex taxonomies, the platform supports hierarchical classification. It traverses the ontology tree top-down, calling classify() once per level with the labels available at that level (available_labels) and the labels already selected (current_path).
Consider this multi-level taxonomy:
Level 1: Financial
Level 2: Accounts Payable
Level 3: Invoice
Level 3: Credit Note
Level 2: Accounts Receivable
Level 3: Sales Invoice
Level 3: Receipt
Level 1: Legal
Level 2: Contracts
Level 3: Service Agreement
Level 3: NDAThe platform processes it as follows:
Level 0: available_labels=["Financial", "Legal"], current_path=[]
-> Plugin returns "Financial"
Level 1: available_labels=["Accounts Payable", "Accounts Receivable"], current_path=["Financial"]
-> Plugin returns "Accounts Payable"
Level 2: available_labels=["Invoice", "Credit Note"], current_path=["Financial", "Accounts Payable"]
-> Plugin returns "Invoice"
Final classification: ["Financial", "Accounts Payable", "Invoice"]At each level the platform passes the valid options as the available_labels argument — reference that list in your prompt rather than hardcoding categories, so the same plugin works at every level. The platform builds the full path and persists it.
Built-In Alternative: SinglePromptClassificationPlugin
If you don't need custom classification logic, consider the built-in SinglePromptClassificationPlugin. It uses a single prompt for all hierarchy levels and requires only a classification_prompt_id in your pipeline config.
When to use it:
- Your classification works well with a single prompt template across all ontology levels
- You don't need custom pre/post-processing logic
- You want to get started quickly without writing plugin code
When to write your own:
- You need different logic at different hierarchy levels
- You need custom pre-processing (e.g., content extraction before classification)
- You want to combine multiple signals beyond LLM output
Automatic model-aware prompt resolution. Whether you use SinglePromptClassificationPlugin or your own plugin, the platform automatically resolves model-specific prompts at runtime. If you register prompt variants with the same purpose but different model_affinity values (see Create a Prompt), the platform swaps in the correct variant for the active LLM model. Plugins see the resolved prompt ID transparently — no code changes needed.
Common Mistakes
1. Using the old execute() method
ClassificationPlugin requires classify(), not execute(). The generic execute() method was removed in SDK 1.0.
# WRONG - old v1.0 API
async def execute(self, context: PluginContext):
for doc in context.documents:
await self.add_document_classification(doc, labels=["invoice"])
return context.documents
# CORRECT - return a label from classify()
async def classify(self, document, file_data, mime_type, available_labels, current_path, configs):
return "invoice" # the platform handles persistence2. Returning a list instead of a string
# WRONG - classify() returns str | None, not a list
return ["invoice", "financial"]
# CORRECT - return a single label
return "invoice"3. Missing bizsupply_sdk import
# WRONG - missing import causes registration failure
class MyClassifier(ClassificationPlugin): # NameError
...
# CORRECT - import the base class
from bizsupply_sdk import ClassificationPlugin
class MyClassifier(ClassificationPlugin):
...4. Forgetting to await
prompt_llm and get_prompt are async — you must await them. Without await you get back a coroutine object instead of the result.
# WRONG - returns a coroutine, not the parsed response
result = self.prompt_llm(prompt="...")
# CORRECT
result = await self.prompt_llm(prompt="...")Next Steps
- Create an Extraction Plugin — pull structured data from classified documents
- Create a Prompt — manage classification prompt templates separately
- Use Plugins — wire your classifier into a full pipeline
- Create a Benchmark — measure and compare classification accuracy
- Plugin Service API — all available service methods