This guide covers creating, registering, and using custom operators in docpipe. Custom operators extend docpipe’s functionality by allowing you to add your own data processing logic to pipelines.
Custom operators can be provided in three ways:
.py files in a directory and register the pathpip install, use everywhereBefore creating custom operators, ensure you have:
pip install docling-pipelinesNew to Docling Pipelines? Complete the USER_GUIDE_PIPELINE_SETUP.md first to understand the basics of flows and operators.
Let’s create a simple operator that adds a greeting column to your data.
A ready-made example operator is available at examples/custom_operators/hello_operator.py.
It adds a greeting column to the table — a minimal but complete custom operator.
Use it directly, or copy it as a starting point for your own operator:
# Use the example directly
export DOCPIPE_CUSTOM_OPERATORS="./examples/custom_operators/hello_operator.py"
# Or copy it to your own workspace
cp examples/custom_operators/hello_operator.py ~/my_custom_operators/hello_operator.py
Point DOCPIPE_CUSTOM_OPERATORS at a file or a directory:
# Single file
export DOCPIPE_CUSTOM_OPERATORS="./examples/custom_operators/hello_operator.py"
# Directory (scans recursively for .py files)
export DOCPIPE_CUSTOM_OPERATORS="$HOME/my_custom_operators"
Create a flow JSON file (hello_flow.json):
{
"flow_name": "Hello Custom Operator Flow",
"description": "Example flow using custom hello operator",
"global_config": {
"doc_column": "content",
"disable_validation": false
},
"flow": [
{
"name": "ingest_1",
"type": "ingest_source",
"config": {
"provider": "filesystem",
"connection_params": {"paths": ["./sample_documents"]}
}
},
{
"name": "hello_1",
"type": "hello",
"config": {},
"depends_on": ["ingest_1"]
}
]
}
Run the flow:
docling-pipelines --flow-file hello_flow.json
Check that your operator is registered:
# List all operators (look for owner="custom")
docling-pipelines --list-operators
# Detailed view with parameters
docling-pipelines --list-operators --verbose
You should see output like:
Operator: hello
Category: Functional
Owner: custom
Label: Hello Operator
Congratulations! You’ve created and run your first custom operator. Now let’s understand how it works.
Every custom operator needs these imports:
# Core operator classes
from docpipe.core.operators.abstract_operator import AbstractOperator, OperatorCategory
# Constants for metadata and configuration
from docpipe.core.constants.constants import DocpipeConstants, AttributeDataTypes
from docpipe.core.constants.operator_constants import OperatorConstants
# Data handling
import pyarrow as pa
from typing import Any
Optional imports (depending on your needs):
# For logging
import logging
# For advanced type hints
from collections.abc import Callable
# For working with PyArrow schemas
import pyarrow.compute as pc
Custom operators must:
AbstractOperatorshort_name, category, ownertransform(), get_metadata(), get_required_features()class MyCustomOperator(AbstractOperator):
# Unique identifier used in flow JSON files
short_name: str = "my_custom"
# Operator category: Extract, Ingest, Functional, Quality, VectorDB, Storage
category: OperatorCategory = OperatorCategory.Functional
# Identifies as custom operator (always use this constant)
owner: str | None = DocpipeConstants.OWNER_CUSTOM
Important: The short_name must be unique. If it conflicts with a built-in operator, your custom operator will override it (first discovered wins).
| Method | Purpose | When Called |
|---|---|---|
transform() |
Process data | During pipeline execution |
get_metadata() |
Define configuration schema | During flow authoring/validation |
get_required_features() |
Declare input dependencies | During flow validation |
validate() |
Runtime validation (optional) | Before pipeline execution |
The transform() method processes PyArrow tables and returns results with metadata.
Method Signature:
def transform(self, table: pa.Table, file_name: str | None = None) -> tuple[list[pa.Table], dict[str, Any]]:
"""
Process the input table and return transformed results.
Args:
table: Input PyArrow table with data to process
file_name: Optional filename for context (e.g., for logging)
Returns:
Tuple of (list of output tables, metadata dictionary)
"""
Basic Pattern:
def transform(self, table: pa.Table, file_name: str | None = None) -> tuple[list[pa.Table], dict[str, Any]]:
# 1. Process the table (your custom logic here)
result_table = table # Replace with your transformation
# 2. Create metadata
metadata = self.create_base_metadata(total_docs_count=table.num_rows)
metadata["processed_docs"] = table.num_rows
metadata["custom_metric"] = 42 # Add your custom metrics
# 3. Return results
return [result_table], metadata
Common Patterns:
def transform(self, table: pa.Table, file_name: str | None = None):
# Create new column data
new_values = [compute_value(row) for row in range(table.num_rows)]
# Add column to table
table = table.append_column("new_column", pa.array(new_values))
return [table], self.create_base_metadata(total_docs_count=table.num_rows)
def transform(self, table: pa.Table, file_name: str | None = None):
# Filter based on condition
mask = pc.greater(table["score"], 0.5)
filtered_table = table.filter(mask)
metadata = self.create_base_metadata(total_docs_count=filtered_table.num_rows)
metadata["filtered_out"] = table.num_rows - filtered_table.num_rows
return [filtered_table], metadata
def transform(self, table: pa.Table, file_name: str | None = None):
# Split based on condition
high_quality = table.filter(pc.greater(table["score"], 0.8))
low_quality = table.filter(pc.less_equal(table["score"], 0.8))
metadata = self.create_base_metadata(total_docs_count=table.num_rows)
return [high_quality, low_quality], metadata
The get_metadata() method defines your operator’s configuration interface. Start with this basic structure:
@staticmethod
def get_metadata() -> dict[str, Any]:
"""Return operator metadata for UI display and validation."""
return {
# Operator category
OperatorConstants.Misc.CATEGORY: MyCustomOperator.category.value,
# Availability flag
OperatorConstants.Misc.IS_OPERATOR_AVAILABLE: True,
# Display name for UI
OperatorConstants.Misc.LABEL: "My Custom Operator",
# Output columns this operator produces (see Advanced Topics)
OperatorConstants.Config.FEATURES: {},
# Input parameters for configuration (see Advanced Topics)
OperatorConstants.Config.ATTRIBUTES: {},
}
For now, you can leave FEATURES and ATTRIBUTES empty. We’ll cover these in detail in the Advanced Topics section.
This method declares which columns must exist in the input table.
@staticmethod
def get_required_features() -> list[str]:
"""
Return list of required input columns.
These are columns that must be present in the PyArrow table
coming from upstream operators.
"""
return [] # No requirements
Examples:
# Operator needs content column
@staticmethod
def get_required_features() -> list[str]:
return ["content"]
# Operator needs multiple columns
@staticmethod
def get_required_features() -> list[str]:
return ["content", "doc_id", "metadata"]
# Operator needs chunked content from ChunkerOperator
@staticmethod
def get_required_features() -> list[str]:
return ["chunked_content"]
Choose based on your use case:
| Scenario | Recommended Method |
|---|---|
| 🏠 Local development | Environment variable (filesystem) |
| 📦 Sharing across projects/teams | Python package (pip install) |
| 🔧 Programmatic control needed | Python API |
| 👥 Team sharing operators | Python package or environment variable + version control |
| ☁️ Enterprise cloud deployment | S3 URIs (see Advanced Topics) |
| 🚀 Public distribution | Python package published to PyPI |
For most users: Use environment variable for development, Python packages for distribution.
Set the DOCPIPE_CUSTOM_OPERATORS environment variable to point to your operators directory:
# Single directory
export DOCPIPE_CUSTOM_OPERATORS="/path/to/custom_operators"
# Multiple directories (colon-separated on Unix, semicolon on Windows)
export DOCPIPE_CUSTOM_OPERATORS="/path/to/operators1:/path/to/operators2"
# Run your flow
docling-pipelines --flow-file flow.json
Make it permanent (add to ~/.bashrc or ~/.zshrc):
echo 'export DOCPIPE_CUSTOM_OPERATORS="$HOME/my_custom_operators"' >> ~/.bashrc
source ~/.bashrc
Verify immediately:
docling-pipelines --list-operators | grep "custom"
Register operators programmatically in your Python code:
from docpipe.lib.docpipe_flow_manager import DocpipeFlowManager
# Create flow manager
manager = DocpipeFlowManager(flow_file="flow.json")
# Register custom operators (using filesystem paths)
manager.register_custom_operators(package_names=["/path/to/custom_operators"])
# Execute flow
result = manager.execute()
Note: Supports filesystem paths, S3 URIs, and installed Python package names.
After registering, verify your operators are loaded:
Using CLI:
# List all operators (custom operators show owner="custom")
docling-pipelines --list-operators
# Detailed view with parameters
docling-pipelines --list-operators --verbose
Using Python:
from docpipe.lib.docpipe_flow_manager import DocpipeFlowManager
# List all operators
print(DocpipeFlowManager.list_operators())
# Detailed view
print(DocpipeFlowManager.list_operators(verbose=True))
What to look for:
short_name appears in the listowner field shows "custom"category is displayedshort_name as a built-in operator, the custom one takes precedenceReference custom operators by their short_name in flow JSON files:
{
"flow_name": "My Flow with Custom Operator",
"description": "Example flow using custom operator",
"global_config": {
"doc_column": "content",
"disable_validation": false
},
"flow": [
{
"name": "ingest_1",
"type": "ingest_source",
"config": {
"provider": "filesystem",
"connection_params": {"paths": ["./documents"]}
}
},
{
"name": "custom_1",
"type": "my_custom",
"config": {
"param_name": "value"
},
"depends_on": ["ingest_1"]
}
]
}
Run the flow:
export DOCPIPE_CUSTOM_OPERATORS="/path/to/custom_operators"
docling-pipelines --flow-file flow.json
Now let’s explore the full power of get_metadata() for defining operator parameters and outputs.
Key Concept:
batch_size, threshold, modecustom_score, processed_textVisual Example:
Flow: Ingest → MyOperator → VectorDB
MyOperator ATTRIBUTES (inputs): MyOperator FEATURES (outputs):
├─ mode: "advanced" ├─ custom_score (added to table)
├─ threshold: 0.8 └─ processed_text (added to table)
└─ batch_size: 100 ↓
Available for VectorDB operator
Features define the new columns your operator adds to the PyArrow table:
OperatorConstants.Config.FEATURES: {
"custom_score": {
OperatorConstants.Misc.NAME: "Custom Score",
OperatorConstants.Config.DESCRIPTION: "Computed quality score for the document",
OperatorConstants.Misc.TYPE: AttributeDataTypes.FLOAT,
OperatorConstants.Misc.TAGS: [OperatorConstants.Misc.MANDATORY],
OperatorConstants.Config.AVAILABLE_FOR_VECTOR_DB: True,
},
"processed_text": {
OperatorConstants.Misc.NAME: "Processed Text",
OperatorConstants.Config.DESCRIPTION: "Text after custom processing",
OperatorConstants.Misc.TYPE: AttributeDataTypes.STRING,
OperatorConstants.Misc.TAGS: [OperatorConstants.Misc.MANDATORY],
},
}
Feature Fields:
name: Display name for UIdescription: Clear explanation of what the feature containstype: Data type (string, integer, float, list, etc.)tags: List of tags (MANDATORY for required features, INTERNAL_FEATURE for system fields)available_for_vector_db: (Optional) Boolean for vector database compatibilityAttributes define the configuration parameters users can set:
Required fields for all attributes:
name: Display name for UIdescription: Clear explanation of the parameter’s purposetype: Data type (string, integer, float, boolean, json, list)required: Whether parameter is mandatory (True/False)default: Default value if not providedOptional fields for validation:
valid_values: List of allowed values (enum-like parameters)min_value/max_value: Numeric range constraintsComplete Example with All Parameter Types:
OperatorConstants.Config.ATTRIBUTES: {
# String parameter with valid values (enum-like)
"mode": {
OperatorConstants.Misc.NAME: "Processing Mode",
OperatorConstants.Config.DESCRIPTION: "Mode for processing documents",
OperatorConstants.Config.REQUIRED: True,
OperatorConstants.Config.DEFAULT: "standard",
OperatorConstants.Config.VALID_VALUES: ["standard", "advanced", "custom"],
OperatorConstants.Misc.TYPE: AttributeDataTypes.STRING,
},
# Integer parameter with range validation
"batch_size": {
OperatorConstants.Misc.NAME: "Batch Size",
OperatorConstants.Config.DESCRIPTION: "Number of documents to process in each batch",
OperatorConstants.Config.REQUIRED: False,
OperatorConstants.Config.DEFAULT: 100,
OperatorConstants.Filtering.MIN_VALUE: 1,
OperatorConstants.Filtering.MAX_VALUE: 1000,
OperatorConstants.Misc.TYPE: AttributeDataTypes.INTEGER,
},
# Float parameter with range validation
"threshold": {
OperatorConstants.Misc.NAME: "Confidence Threshold",
OperatorConstants.Config.DESCRIPTION: "Minimum confidence score (0.0-1.0)",
OperatorConstants.Config.REQUIRED: False,
OperatorConstants.Config.DEFAULT: 0.8,
OperatorConstants.Filtering.MIN_VALUE: 0.0,
OperatorConstants.Filtering.MAX_VALUE: 1.0,
OperatorConstants.Misc.TYPE: AttributeDataTypes.FLOAT,
},
# Boolean parameter (no validation needed)
"enable_feature": {
OperatorConstants.Misc.NAME: "Enable Feature",
OperatorConstants.Config.DESCRIPTION: "Whether to enable advanced feature processing",
OperatorConstants.Config.REQUIRED: False,
OperatorConstants.Config.DEFAULT: False,
OperatorConstants.Misc.TYPE: AttributeDataTypes.BOOLEAN,
},
# JSON parameter for complex configuration
"custom_config": {
OperatorConstants.Misc.NAME: "Custom Configuration",
OperatorConstants.Config.DESCRIPTION: "JSON object with custom settings",
OperatorConstants.Config.REQUIRED: False,
OperatorConstants.Config.DEFAULT: None,
OperatorConstants.Misc.TYPE: AttributeDataTypes.JSON,
},
}
Accessing Parameters in transform():
Parameters defined in ATTRIBUTES are injected as instance variables at runtime. Access them using getattr() with a default value — direct attribute access (e.g. self.batch_size) will raise an AttributeError if the attribute is not present.
def transform(self, table: pa.Table, file_name: str | None = None):
mode = getattr(self, "mode", "standard")
enable_feature = getattr(self, "enable_feature", False)
batch_size = getattr(self, "batch_size", 100)
if mode == "advanced":
# Use advanced processing
pass
if enable_feature:
# Feature is enabled
pass
# Use numeric parameters
for i in range(0, table.num_rows, batch_size):
batch = table.slice(i, batch_size)
# Process batch
The validate() method performs runtime validation before operator execution. This is optional but recommended for robust operators.
Method Signature:
def validate(self, errors: list[str], warnings: list[str], available_features: list[str]) -> None:
"""
Validate operator configuration at runtime.
Args:
errors: List to append blocking errors (prevent execution)
warnings: List to append non-critical warnings (allow execution)
available_features: List of columns available from upstream operators
"""
When to Use:
Basic Pattern:
def validate(self, errors: list[str], warnings: list[str], available_features: list[str]) -> None:
# 1. ALWAYS call parent first to validate required features
super().validate(errors, warnings, available_features)
# 2. Validate required fields
if not self.mode:
errors.append("mode is required for MyCustomOperator")
# 3. Validate value ranges
if self.batch_size < 1 or self.batch_size > 1000:
errors.append("batch_size must be between 1 and 1000")
# 4. Check feature availability
if "content" not in available_features:
errors.append("'content' column must be available from upstream operators")
# 5. Warn about potential conflicts
if "custom_score" in available_features:
warnings.append("'custom_score' column already exists and will be overwritten")
Common Validation Patterns:
if self.should_validate_field(field_value=self.mode):
if self.mode not in ["standard", "advanced", "custom"]:
errors.append("mode must be one of: standard, advanced, custom")
if self.enable_advanced and not self.custom_config:
errors.append("custom_config is required when enable_advanced is True")
if self.should_validate_field(field_value=self.api_endpoint):
try:
response = requests.get(self.api_endpoint, timeout=5)
if response.status_code != 200:
errors.append(f"API endpoint {self.api_endpoint} is not accessible")
except Exception as e:
errors.append(f"Failed to connect to API: {str(e)}")
Key Differences: validate() vs get_metadata()
| Aspect | validate() |
get_metadata() |
|---|---|---|
| When Called | Runtime (before execution) | Design-time (flow authoring) |
| Purpose | Validate actual values | Declare parameter schemas |
| Context | Has available_features |
No execution context |
| Validation Type | Instance-specific | Schema-level (types, ranges) |
Best Practices:
super().validate() firstshould_validate_field() for expensive validation checksPackage-based operators allow you to distribute custom operators as standard Python packages that can be installed via pip. This is the recommended approach for sharing operators across teams or projects.
A custom operator package follows standard Python package structure:
my_custom_operators/
├── pyproject.toml # Package configuration with entry points
├── README.md # Package documentation
└── my_custom_operators/ # Package source
├── __init__.py
└── operators/ # Operators module
├── __init__.py
├── operator1.py
└── operator2.py
Complete example available at: examples/custom_operators/package_example/
Step 1: Create pyproject.toml
[project]
name = "my-custom-operators"
version = "0.1.0"
description = "Custom operators for Docling Pipelines"
requires-python = ">=3.12"
dependencies = [
"docling-pipelines>=1.0.0",
]
# Register operators via entry points
[project.entry-points."docpipe.operators"]
my_operator = "my_custom_operators.operators.my_operator:MyOperator"
another_operator = "my_custom_operators.operators.another:AnotherOperator"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["my_custom_operators"]
Key Points:
"docpipe.operators" group enable automatic discoveryoperator_name = "package.module:ClassName"Step 2: Create Operator Files
Place your operators in the operators/ subdirectory:
# my_custom_operators/operators/my_operator.py
from typing import Any
import pyarrow as pa
from docpipe.core.operators.abstract_operator import AbstractOperator, OperatorCategory
from docpipe.core.constants.constants import DocpipeConstants
class MyOperator(AbstractOperator):
short_name: str = "my_operator"
category: OperatorCategory = OperatorCategory.Functional
owner: str | None = DocpipeConstants.OWNER_CUSTOM
def __init__(self, config: dict[str, Any]):
super().__init__(config)
# Your initialization
def transform(self, table: pa.Table, file_name: str | None = None):
# Your transformation logic
return [table], self.create_base_metadata(total_docs_count=table.num_rows)
@staticmethod
def get_metadata() -> dict[str, Any]:
return {
"short_name": MyOperator.short_name,
"category": MyOperator.category.value,
"description": "My custom operator",
}
Step 3: Build and Install
# Install in development mode (editable)
pip install -e .
# Or build and install
pip install .
# Or build for distribution
pip install build
python -m build
# Creates dist/my_custom_operators-0.1.0.tar.gz and .whl
Method 1: Automatic Discovery (Recommended)
Once installed, operators are automatically discovered:
# Install the package
pip install my-custom-operators
# Operators are automatically available
docling-pipelines --list-operators | grep "my_operator"
# Use in flows without any registration
docling-pipelines --flow-file flow.json
Method 2: Explicit Registration in Flow
You can also explicitly register packages in flow configuration:
{
"flow_name": "My Flow",
"custom_operators": {
"adapters": [
{
"type": "package",
"package_name": "my_custom_operators",
"operator_module": "operators"
}
]
},
"flow": [...]
}
Method 3: Programmatic Registration
from docpipe.lib.docpipe_flow_manager import DocpipeFlowManager
manager = DocpipeFlowManager(flow_file="flow.json")
manager.register_custom_operators(paths=["my_custom_operators"])
result = manager.execute()
The package adapter discovers operators through two methods:
pyproject.toml
Both methods work together - operators found via either method are loaded.
To share your operators publicly:
# Install twine
pip install twine
# Build the package
python -m build
# Upload to PyPI
twine upload dist/*
# Others can now install
pip install my-custom-operators
---
### Common Issues and Solutions
This section documents key learnings and gotchas discovered during package adapter implementation.
#### 1. Adapter Registration
**Issue**: PackageAdapter not discovering operators even though entry points are configured correctly.
**Root Cause**: The `PackageAdapter` class must be imported in `src/docpipe/core/orchestration/operator_loader/adapters/__init__.py` for the `@register_operator_source` decorator to work.
**Solution**: Ensure the adapter is imported in the adapters module:
```python
# src/docpipe/core/orchestration/operator_loader/adapters/__init__.py
from .package_adapter import PackageAdapter # Required for registration
from .filesystem_adapter import FilesystemAdapter
Without this import, the decorator won’t execute during module initialization, and the adapter won’t be registered with the loader service.
Issue: Custom operators not appearing in docling-pipelines --list-operators output, or appearing without proper identification.
Root Cause: Custom operators must include the "owner" field in their metadata to distinguish them from built-in docpipe operators.
Solution: Always include the owner field in your operator’s get_metadata() method:
@classmethod
def get_metadata(cls) -> dict:
return {
"operator_id": "my_custom_operator",
"name": "My Custom Operator",
"owner": "my_company", # Required for custom operators
"version": "1.0.0",
"description": "Does something useful",
# ... other metadata
}
The operator listing uses this field to categorize operators by owner in the output.
Issue: Entry points configured but operators not discovered by PackageAdapter.
Root Cause: The entry points must use the exact group name "docpipe.operators" in pyproject.toml.
Solution: Use the correct group name in your package configuration:
[project.entry-points."docpipe.operators"]
uppercase_operator = "my_custom_operators.operators.uppercase_operator:UppercaseOperator"
reverse_operator = "my_custom_operators.operators.reverse_operator:ReverseOperator"
Incorrect examples that won’t work:
[project.entry-points."docpipe.operator"] (missing ‘s’)[project.entry-points."docpipe_operators"] (underscore instead of dot)[project.entry-points."custom.operators"] (wrong prefix)Issue: Entry points registered but not discovered for a specific package.
Root Cause: PackageAdapter filters entry points by checking if entry_point.value.startswith(package_name). The entry point value must start with the package name to be discovered.
Example:
# Package name: my_custom_operators
[project.entry-points."docpipe.operators"]
# ✅ Correct - value starts with package name
uppercase_operator = "my_custom_operators.operators.uppercase_operator:UppercaseOperator"
# ❌ Wrong - value doesn't start with package name
uppercase_operator = "operators.uppercase_operator:UppercaseOperator"
Solution: Ensure entry point values use the full module path starting with your package name:
# When registering with DocpipeFlowManager
manager.register_custom_operators(packages=["my_custom_operators"])
# Entry point value must start with "my_custom_operators"
Issue: TypeError: 'EntryPoints' object is not subscriptable when running on Python 3.12+.
Root Cause: The older entry_points()["group_name"] syntax is deprecated in Python 3.12+.
Solution: Use the group parameter instead:
# ❌ Old syntax (Python < 3.12)
from importlib.metadata import entry_points
eps = entry_points()["docpipe.operators"]
# ✅ New syntax (Python 3.12+)
from importlib.metadata import entry_points
eps = entry_points(group="docpipe.operators")
The PackageAdapter implementation uses the new syntax for compatibility.
Issue: ModuleNotFoundError when loading operators from installed package.
Common Causes:
Solutions:
# Verify package is installed
pip list | grep my-custom-operators
# Install in editable mode for development
pip install -e /path/to/package
# Or install from PyPI
pip install my-custom-operators
# Check if operators are discoverable
docling-pipelines --list-operators
Issue: Operators not appearing in listing or not being loaded.
Debugging Steps:
from importlib.metadata import entry_points
eps = entry_points(group="docpipe.operators")
for ep in eps:
print(f"{ep.name}: {ep.value}")
from my_custom_operators.operators.my_operator import MyOperator
print(MyOperator.get_metadata())
# Should include "owner" field
from docpipe.core.orchestration.operator_factory import OperatorFactory
factory = OperatorFactory()
factory.register_custom_operators(packages=["my_custom_operators"])
# Try to get your operator
operator = factory.get_operator("my_operator_id")
DS_LOG_LEVEL=DEBUG docling-pipelines --list-operators
__init__.py for decorator registration"docpipe.operators" in entry points
**For private repositories:**
```bash
# Upload to private PyPI server
twine upload --repository-url https://your-pypi-server.com dist/*
# Install from private repository
pip install my-custom-operators --index-url https://your-pypi-server.com
Use semantic versioning in pyproject.toml:
[project]
version = "1.2.3" # MAJOR.MINOR.PATCH
Users can install specific versions:
pip install my-custom-operators==1.2.3
pip install my-custom-operators>=1.0.0,<2.0.0
Package not found:
# Verify package is installed
pip show my-custom-operators
# Check installed location
pip show -f my-custom-operators
Operators not discovered:
# Verify entry points are registered
python -c "import importlib.metadata; print(list(importlib.metadata.entry_points(group='docpipe.operators')))"
# Check operator module can be imported
python -c "from my_custom_operators.operators import MyOperator; print(MyOperator.short_name)"
Import errors:
pyproject.tomlS3 support is an optional advanced feature for enterprise deployments that need centralized operator management.
# Using uv
uv pip install boto3
Option 1: Environment variables
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key" # pragma: allowlist secret
export AWS_DEFAULT_REGION="us-east-1" # Optional
Option 2: Credentials file (~/.aws/credentials)
[default]
aws_access_key_id = your-access-key
aws_secret_access_key = your-secret-key
Option 3: IAM roles (automatic for EC2/ECS/Lambda instances)
s3://bucket-name/path/to/operators/
Operators are downloaded to ~/.docpipe/custom_operators_cache/ and loaded from cache.
Environment variable:
export DOCPIPE_CUSTOM_OPERATORS="s3://my-company-operators/production/"
docling-pipelines --flow-file flow.json
Programmatic:
from docpipe.lib.docpipe_flow_manager import DocpipeFlowManager
manager = DocpipeFlowManager(flow_file="flow.json")
manager.register_custom_operators(package_names=["s3://my-company-operators/production/"])
result = manager.execute()
Mixed sources (filesystem + S3):
export DOCPIPE_CUSTOM_OPERATORS="/local/path:s3://bucket/path"
Unique short_name: Ensure your operator’s short_name doesn’t conflict with built-in operators unless you intend to override them
Proper validation: Implement get_required_features() to validate input columns and validate() for runtime checks
Comprehensive metadata: Provide complete metadata with clear descriptions for all parameters
Error handling: Use record_failed_document() and record_skipped_document() for tracking processing issues
Testing: Test operators independently before integrating into flows
Documentation: Include docstrings and comments explaining your operator’s purpose and usage
Type hints: Use proper type hints for better IDE support and code clarity
Logging: Use the logging module for debugging and monitoring
Safe attribute access: Always use getattr(self, "param_name", default) to access configuration attributes in transform() — direct attribute access will raise AttributeError at runtime
Operator not found:
DOCPIPE_CUSTOM_OPERATORS is set correctly.py filesAbstractOperatorshort_name matches what you’re using in the flowImport errors:
pip show docling-pipelinesS3 access denied:
pip show boto3Duplicate operator warning:
short_name as built-in operators will override themValidation errors:
Operator not appearing in list:
docling-pipelines --list-operators to verify registrationshort_name, category, owner) are definedAttributeError: 'MyOperator' object has no attribute '...':
self.param) with getattr(self, "param", default_value)self — getattr() provides a safe fallbackA complete working example is available in the Docling Pipelines repository at:
examples/custom_operators/example_custom_operator.pyTo use the example:
# Clone the Docling Pipelines repository
git clone https://github.com/your-org/docling-pipelines.git
# Navigate to examples
cd docling-pipelines/examples/custom_operators
# Register and use
export DOCPIPE_CUSTOM_OPERATORS="$(pwd)"
docling-pipelines --list-operators | grep example
For more details on the base class implementation and loading mechanism, refer to:
src/docpipe/core/operators/abstract_operator.py - Base operator classsrc/docpipe/core/operators/operator_registry.py - Operator registration systemsrc/docpipe/core/operators/ - Real-world examples