This guide explains how external applications can integrate their own operators with the docling-pipelines framework when it’s installed as a wheel package.
The docling-pipelines package provides a plugin hook pattern that allows host applications to inject their own operators into the operator registry. This enables seamless integration of custom operators without modifying the docpipe codebase.
External Application
├── docling-pipelines (installed wheel)
│ └── operator_registry.py (provides hooks)
└── custom_operators/
├── my_operator.py
└── __init__.py (registers operators)
# external_app/operators/my_custom_operator.py
from docpipe.core.operators.abstract_operator import AbstractOperator
import pyarrow as pa
class MyCustomOperator(AbstractOperator):
"""Custom operator for external application."""
short_name = "my_custom_op"
def __init__(self, *, config: dict):
super().__init__(config=config)
# Your initialization
def transform(self, table: pa.Table) -> pa.Table:
"""Transform logic."""
# Your transformation logic
return table
@classmethod
def is_available(cls) -> bool:
"""Check if operator dependencies are available."""
return True
# external_app/operators/__init__.py
from external_app.operators.my_custom_operator import MyCustomOperator
# Define your application's operator frozenset
APP_OPERATORS = frozenset({
MyCustomOperator,
# Add more operators here
})
# external_app/__init__.py or main.py
from docpipe.core.operators.operator_registry import register_operator_provider
from external_app.operators import APP_OPERATORS
def get_app_operators(orchestrator=None):
"""
Provider function that returns application operators.
Args:
orchestrator: Optional orchestrator type ("python", "spark")
Returns:
frozenset: Set of operator classes
"""
# Optional: Filter by orchestrator
if orchestrator == "spark":
return frozenset({
op for op in APP_OPERATORS
if hasattr(op, 'supports_spark') and op.supports_spark
})
return APP_OPERATORS
# Register at application startup (before using docpipe)
register_operator_provider(get_app_operators)
# external_app/pipeline.py
from docpipe.lib.docpipe_flow_manager import DocpipeFlowManager
# Your custom operator is now available in flows
flow_def = {
"flow_name": "My Pipeline",
"flow": [
{
"type": "ingest_source",
"name": "ingest",
"config": {"provider": "filesystem", "connection_params": {"paths": ["./data"]}}
},
{
"type": "my_custom_op", # Your custom operator!
"name": "custom_processing",
"config": {"param1": "value1"},
"depends_on": ["ingest"]
}
]
}
manager = DocpipeFlowManager(flow_def=flow_def)
result = manager.execute()
Filter operators based on orchestrator type:
def get_app_operators(orchestrator=None):
"""Return operators filtered by orchestrator."""
if orchestrator == "python":
return frozenset({
PythonOnlyOperator,
SharedOperator,
})
elif orchestrator == "spark":
return frozenset({
SparkOnlyOperator,
SharedOperator,
})
# Return all if orchestrator not specified
return APP_OPERATORS
Register multiple operator sources:
from docpipe.core.operators.operator_registry import register_operator_provider
# Register core application operators
register_operator_provider(get_core_operators)
# Register plugin operators
register_operator_provider(get_plugin_operators)
# Register environment-specific operators
register_operator_provider(get_env_specific_operators)
Docling Pipelines uses a priority-based resolution system to handle operators with the same short_name. Operators are assigned priorities based on their owner attribute:
Built-in Priority Levels (lower number = higher priority):
Additional tiers can be registered at runtime — see Registering a Custom Priority Tier below.
from docpipe.core.constants.constants import DocpipeConstants
class CustomExtractOperator(AbstractOperator):
"""Custom extract operator with priority."""
short_name = "extract" # Same as docpipe's ExtractOperator
owner = DocpipeConstants.OWNER_CUSTOM # Priority 100
def transform(self, table: pa.Table) -> pa.Table:
# Custom extraction logic
return table
# This custom operator will override docpipe's built-in extract operator
class MyExtractOperator(AbstractOperator):
short_name = "extract"
owner = DocpipeConstants.OWNER_CUSTOM # Priority 100 beats inbuilt priority 200
def transform(self, table: pa.Table) -> pa.Table:
# Your custom logic replaces docpipe's extract
return table
If you need an operator tier with higher precedence than OWNER_CUSTOM, register it at startup before operators are loaded. Priorities below 100 outrank all built-in tiers.
from docpipe.core.orchestration.operator_factory import OperatorFactory
# Call once at application startup, before operators are loaded
OperatorFactory.register_owner_priority(owner="my_app", priority=10)
class MyAppOperator(AbstractOperator):
short_name = "extract"
owner = "my_app" # Priority 10 — overrides custom (100) and inbuilt (200)
def transform(self, table: pa.Table) -> pa.Table:
return table
Priority ranges:
| Range | Purpose |
|---|---|
| 0-99 | Consumer tiers above all built-ins |
| 100 | OWNER_CUSTOM |
| 101-199 | Consumer tiers between custom and Docling Pipelines built-ins |
| 200 | OWNER_DOCPIPE |
Note: If you don’t set the owner attribute, it defaults to OWNER_CUSTOM (priority 100).
Load operators dynamically based on configuration:
def get_app_operators(orchestrator=None):
"""Dynamically load operators based on config."""
import os
from importlib import import_module
operators = set()
# Load from environment variable
operator_modules = os.getenv("APP_OPERATOR_MODULES", "").split(",")
for module_name in operator_modules:
if module_name.strip():
try:
module = import_module(module_name)
if hasattr(module, "OPERATORS"):
operators.update(module.OPERATORS)
except ImportError as e:
print(f"Failed to load operators from {module_name}: {e}")
return frozenset(operators)
register_operator_provider(provider_func)Register an external operator provider function.
Parameters:
provider_func (callable): Function that returns frozenset of operator classes
provider_func(orchestrator: str | None = None) -> frozensetRaises:
TypeError: If provider_func is not callableExample:
from docpipe.core.operators.operator_registry import register_operator_provider
def my_provider(orchestrator=None):
return frozenset({MyOperator1, MyOperator2})
register_operator_provider(my_provider)
clear_operator_providers()Clear all registered operator providers. Useful for testing.
Example:
from docpipe.core.operators.operator_registry import clear_operator_providers
# Clear all providers
clear_operator_providers()
get_registered_provider_count()Get the number of registered external operator providers.
Returns:
int: Number of registered providersExample:
from docpipe.core.operators.operator_registry import get_registered_provider_count
count = get_registered_provider_count()
print(f"Registered providers: {count}")
get_docpipe_operators(orchestrator=None)Get all operators (Docling Pipelines built-ins + external).
Parameters:
orchestrator (str, optional): Orchestrator type for filteringReturns:
frozenset: Combined set of operator classesExample:
from docpipe.core.operators.operator_registry import get_docpipe_operators
# Get all operators
all_ops = get_docpipe_operators()
# Get Python-specific operators
python_ops = get_docpipe_operators(orchestrator="python")
import pytest
from docpipe.core.operators.operator_registry import (
register_operator_provider,
clear_operator_providers,
get_docpipe_operators
)
@pytest.fixture(autouse=True)
def reset_providers():
"""Reset operator providers before each test."""
clear_operator_providers()
yield
clear_operator_providers()
def test_custom_operator_registration():
"""Test that custom operators are registered correctly."""
def test_provider(orchestrator=None):
return frozenset({MyTestOperator})
register_operator_provider(test_provider)
operators = get_docpipe_operators()
short_names = {op.short_name for op in operators}
assert "my_test_op" in short_names
short_name values to avoid conflictsis_available(): Check dependencies in the is_available() methodProblem: Custom operators not appearing in flows
Solution:
short_name attributeimport logging
logging.basicConfig(level=logging.DEBUG)
# Check logs for operator registration messages
Problem: Custom operator not overriding docpipe operator
Solution:
short_name matches exactlyowner = DocpipeConstants.OWNER_CUSTOM on your operatorExample Debug:
import logging
logging.basicConfig(level=logging.INFO)
# Look for messages like:
# "Operator 'extract': MyExtractOperator (priority=1) overrides ExtractOperator (priority=2)"
# or
# "Operator 'extract': MyExtractOperator (priority=1) cannot override EnterpriseOp (priority=0)"
Problem: Cannot import docpipe modules
Solution:
external_app/
├── pyproject.toml
├── external_app/
│ ├── __init__.py # Register operators here
│ ├── main.py # Application entry point
│ ├── operators/
│ │ ├── __init__.py # Define APP_OPERATORS
│ │ ├── custom_op1.py
│ │ └── custom_op2.py
│ └── flows/
│ └── pipeline.json # Flow using custom operators
└── tests/
└── test_operators.py