Workflow Type Registry — Developer Guide
Table of Contents
- Overview
- Architecture
- Quick Start
- Registry Configuration
- Project-Level Injection
- Model Configuration Schema
- QueryBuilderMixin Integration
- Dynamic Registration API
- Complete Examples
- Troubleshooting
Overview
The WorkflowTypeRegistry is a central service within the Eneza Workflow Engine that maps Workflownames choice values to their allowed Django models for query building. Each workflow type defines which models can be queried and filtered in its conditions.
Problem solved: The workflow engine needs to know which models are available for filtering when building conditional rules. Instead of hardcoding model discovery across multiple views, the registry provides a single source of truth that supports both framework-level defaults and project-level customization.
Key features:
- Framework-level default registry for built-in workflow types
- Project-level injection via workflowconfig.py and MY_PROJECT_WORKFLOWS_CONFIG setting
- Dynamic runtime registration via WorkflowTypeRegistry.register()
- Lazy-loading: project config is loaded on first access
- Fallback to default models (User, Entity) when no workflow is specified
- Generic query builder views handle all workflow types — no custom view creation needed
Architecture
┌─────────────────────────────────────────────────────────────┐
│ WorkflowTypeRegistry (enezaworkflowengine/services/) │
│ - _framework_registry: built-in workflow type definitions │
│ - _default_models: fallback models (User, Entity) │
│ - _load_project_registry(): loads from workflowconfig.py │
│ - _load_merged_registry(): merges framework + project │
│ - get_allowed_models(workflow_name): public API │
│ - has_workflow(workflow_name): check registration │
│ - register(workflow_name, models): dynamic registration │
├─────────────────────────────────────────────────────────────┤
│ Project workflowconfig.py │
│ class WorkflowTypeConfig: │
│ WORKFLOW_TYPE_REGISTRY = { ... } │
│ │
│ Project settings.py │
│ MY_PROJECT_WORKFLOWS_CONFIG = 'myproject.workflowconfig...' │
├─────────────────────────────────────────────────────────────┤
│ QueryBuilderMixin (enezaworkflowengine/views/mixin.py) │
│ - get_allowed_models() → calls WorkflowTypeRegistry │
│ - get_model_filters() → generates filter definitions │
├─────────────────────────────────────────────────────────────┤
│ Generic Query Views (views/querybuilder.py) │
│ - BuildQueryView: generic query builder for any workflow │
│ - SaveQueryView: saves queries with workflow context │
│ - SaveQueryListView/SaveQueryDetailView: manage queries │
├─────────────────────────────────────────────────────────────┤
│ Workflownames choices (models/choices.py) │
│ - Defines available workflow name values │
│ - Supports project-level override via MY_PROJECT_WORKFLOWS_NAME │
└─────────────────────────────────────────────────────────────┘
Data flow (first request):
BuildQueryView.get()
→ self.get_workflow_name() → reads GET/POST 'workflow_name' param
→ self.get_allowed_models()
→ WorkflowTypeRegistry.get_allowed_models(workflow_name)
→ WorkflowTypeRegistry._load_merged_registry()
→ deepcopy(_framework_registry)
→ load MY_PROJECT_WORKFLOWS_CONFIG class
→ merge config.WORKFLOW_TYPE_REGISTRY (if defined)
→ cache result in cls._registry
→ return merged_registry.get(workflow_name, _default_models)
→ BuildQueryView builds filters_by_model_json from allowed models
→ renders query_builder.html with model list and filter config
Quick Start
Built-in Workflow Types
The framework ships with these workflow types pre-configured:
| Workflow Name | Primary Model | App |
|---|---|---|
PurchaseOrderWorkflow |
PurchaseOrder |
purchasing |
InvoiceWorkflow |
Frameworktestdocument |
frameworkapp |
CreditCardWorkflow |
CreditCardTransaction |
accounting |
ExpenseWorkflow |
Expense |
accounting |
LeaveApplicationWorkflow |
LeaveApplication |
hr |
Using the Generic Query Builder
The framework provides BuildQueryView and related generic views that work with any registered workflow type. Access the query builder with a workflow name parameter:
/workflow/build/?workflow_name=ExpenseWorkflow
The generic views automatically load allowed models, generate filter definitions, render the query builder UI, and save queries with workflow context. No custom view code is needed.
Registry Configuration
Model Configuration Dict Structure
Each workflow type maps to a list of model configuration dicts:
{
'slug': 'purchase_order', # Unique identifier (used in JS)
'label': 'Purchase Order', # Human-readable label
'app_label': 'purchasing', # Django app label
'model_name': 'PurchaseOrder', # Django model class name
'relationships': [ # Explicit relationship definitions
{
'field_name': 'vendor', # FK/M2M field name on the primary model
'label': 'Vendor', # Human-readable label
'type': 'fk', # 'fk' or 'm2m'
'related_app': 'purchasing',
'related_model': 'Vendor'
},
]
}
Relationships
The relationships list defines which related models are available for filtering:
| Field | Description |
|---|---|
field_name |
FK or M2M field name on the primary model |
label |
Display label for the relationship group |
type |
'fk' for ForeignKey, 'm2m' for ManyToMany |
related_app |
Django app label of the related model |
related_model |
Class name of the related model |
Empty relationships: If a model has no explicit relationships, use 'relationships': []. The framework will auto-discover relationships from the model's meta unless you explicitly define an empty list.
Project-Level Injection
Overview
Projects using the Eneza framework can extend or override the workflow type registry via a dedicated workflowconfig.py file. This follows the same pattern as MY_PROJECT_MODULES and MY_PROJECT_NUMBER_SEQUENCE_DOCUMENT.
Step 1: Create workflowconfig.py
Create a workflowconfig.py file in your project root (same level as settings.py):
# myproject/workflowconfig.py
class WorkflowTypeConfig:
"""
Project-level workflow type registry configuration.
Define custom workflow types that extend or override framework defaults.
"""
WORKFLOW_TYPE_REGISTRY = {
'SalesOrderWorkflow': [
{
'slug': 'sales_order',
'label': 'Sales Order',
'app_label': 'sales',
'model_name': 'SalesOrder',
'relationships': [
{
'field_name': 'customer',
'label': 'Customer',
'type': 'fk',
'related_app': 'crm',
'related_model': 'Customer',
},
{
'field_name': 'shipping_address',
'label': 'Shipping Address',
'type': 'fk',
'related_app': 'crm',
'related_model': 'Address',
},
],
},
],
'InventoryWorkflow': [
{
'slug': 'inventory_item',
'label': 'Inventory Item',
'app_label': 'inventory',
'model_name': 'InventoryItem',
'relationships': [],
},
],
}
Step 2: Reference in settings.py
Add MY_PROJECT_WORKFLOWS_CONFIG to your project's settings.py:
# myproject/settings.py
MY_PROJECT_WORKFLOWS_CONFIG = 'myproject.workflowconfig.WorkflowTypeConfig'
Merge Behavior
- Project-level definitions take precedence. If a workflow name exists in both the framework registry and the project registry, the project's definition wins entirely.
- Framework registry is immutable. Project config cannot modify individual entries in the framework registry — it can only add new workflow names or completely override existing ones.
- Lazy loading. The merge happens once on the first call to
get_allowed_models(),has_workflow(), orregister(). Subsequent calls use the cached merged registry.
Step 3: Register Workflow Name in Choices
For the workflow to appear in dropdowns and be selectable, add it to your project's workflow name choices:
# myproject/workflowconfig.py (or choices.py)
from enezaworkflowengine.models.choices import Workflownames
class ProjectWorkflowNames(Workflownames):
SALESORDERWORKFLOW = 'SalesOrderWorkflow', 'Sales Order Workflow'
INVENTORYWORKFLOW = 'InventoryWorkflow', 'Inventory Workflow'
# myproject/settings.py
MY_PROJECT_WORKFLOWS_NAME = 'myproject.workflowconfig.ProjectWorkflowNames'
Model Configuration Schema
Full Schema Reference
{
# === Required fields ===
'slug': str, # Unique identifier, used in JS/UI
'label': str, # Human-readable display name
'app_label': str, # Django app label (e.g., 'purchasing')
'model_name': str, # Django model class name (e.g., 'PurchaseOrder')
# === Optional fields ===
'relationships': [ # List of relationship definitions (default: auto-discover)
{
# === Required for each relationship ===
'field_name': str, # FK/M2M field name on primary model
'label': str, # Display label
'type': str, # 'fk' or 'm2m'
'related_app': str, # Related model's app label
'related_model': str, # Related model's class name
},
# ... more relationships
],
}
Relationship Type Behavior
type |
Behavior |
|---|---|
'fk' |
ForeignKey — traverses __ lookups to filter related model fields |
'm2m' |
ManyToMany — same traversal, but uses JOIN through the through-table |
When to Use Explicit vs Auto-Discovered Relationships
Use explicit relationships when: - You want to control which relationships are exposed in the query builder - You want custom labels for relationship groups - You want to limit the depth of relationship traversal - The relationship field name differs from convention
Use auto-discovery (omit relationships key) when:
- All FK and M2M fields on the model should be exposed
- Default verbose_name labels are acceptable
- No filtering of relationships is needed
Use empty list ('relationships': []) when:
- The model has no relationships to expose
- You explicitly want to disable auto-discovery
QueryBuilderMixin Integration
How Generic Views Use the Registry
The framework's generic query builder views (BuildQueryView, SaveQueryView, etc.) read from the registry automatically. No custom view creation is needed.
The mixin determines the workflow from:
1. GET parameter: ?workflow_name=SalesOrderWorkflow
2. POST parameter: workflow_name in form/body
3. View attribute: self.workflow_name (if set on a custom view)
4. None: Falls back to default models (User, Entity)
Generic Views Available
| View | URL Name | Purpose |
|---|---|---|
BuildQueryView |
workflow:build_query |
Query builder UI |
SaveQueryView |
workflow:save_query |
POST endpoint to save queries |
SaveQueryListView |
workflow:savedquery_list |
List saved queries |
SaveQueryDetailView |
workflow:savedquery_detail |
View/edit/execute saved query |
SaveQueryDeleteView |
workflow:savedquery_delete |
Delete a saved query |
Usage
Access the query builder with a workflow parameter:
/workflow/build/?workflow_name=ExpenseWorkflow
The views automatically:
- Load allowed models via WorkflowTypeRegistry.get_allowed_models()
- Generate filter definitions for each model
- Render the query builder UI
- Save queries with workflow context for later filtering
Dynamic Registration API
register(workflow_name, models)
Register a workflow type at runtime (useful in AppConfig.ready() or signal handlers):
from enezaworkflowengine.services.workflow_type import WorkflowTypeRegistry
# In apps.py ready() or initialization module:
WorkflowTypeRegistry.register('MyCustomWorkflow', [
{
'slug': 'custom_model',
'label': 'Custom Model',
'app_label': 'myapp',
'model_name': 'CustomModel',
'relationships': [],
},
])
has_workflow(workflow_name)
Check if a workflow name is registered:
if WorkflowTypeRegistry.has_workflow('SalesOrderWorkflow'):
models = WorkflowTypeRegistry.get_allowed_models('SalesOrderWorkflow')
get_allowed_models(workflow_name=None)
Get allowed models for a workflow:
# For a specific workflow
models = WorkflowTypeRegistry.get_allowed_models('ExpenseWorkflow')
# For default (no workflow)
models = WorkflowTypeRegistry.get_allowed_models()
# or
models = WorkflowTypeRegistry.get_allowed_models(None)
Complete Examples
Example 1: Override a Framework Workflow
To completely override an existing workflow's model configuration:
# myproject/workflowconfig.py
class WorkflowTypeConfig:
WORKFLOW_TYPE_REGISTRY = {
'InvoiceWorkflow': [
{
'slug': 'my_invoice',
'label': 'My Custom Invoice',
'app_label': 'billing',
'model_name': 'MyInvoice',
'relationships': [
{
'field_name': 'client',
'label': 'Client',
'type': 'fk',
'related_app': 'crm',
'related_model': 'Client',
},
],
},
],
}
This replaces the framework's InvoiceWorkflow definition entirely.
Example 2: Add a New Workflow with Multiple Models
# myproject/workflowconfig.py
class WorkflowTypeConfig:
WORKFLOW_TYPE_REGISTRY = {
'ProcurementWorkflow': [
{
'slug': 'purchase_requisition',
'label': 'Purchase Requisition',
'app_label': 'procurement',
'model_name': 'PurchaseRequisition',
'relationships': [
{
'field_name': 'requester',
'label': 'Requester',
'type': 'fk',
'related_app': 'enezacommon',
'related_model': 'User',
},
],
},
{
'slug': 'purchase_order',
'label': 'Purchase Order',
'app_label': 'procurement',
'model_name': 'PurchaseOrder',
'relationships': [
{
'field_name': 'vendor',
'label': 'Vendor',
'type': 'fk',
'related_app': 'procurement',
'related_model': 'Vendor',
},
{
'field_name': 'requisition',
'label': 'Requisition',
'type': 'fk',
'related_app': 'procurement',
'related_model': 'PurchaseRequisition',
},
],
},
],
}
class ProjectWorkflowNames(Workflownames):
PROCUREMENTWORKFLOW = 'ProcurementWorkflow', 'Procurement Workflow'
Example 3: Runtime Registration in AppConfig
For apps that want to register their own workflow types without requiring project config:
# myapp/apps.py
from django.apps import AppConfig
class MyAppConfig(AppConfig):
name = 'myapp'
def ready(self):
from enezaworkflowengine.services.workflow_type import WorkflowTypeRegistry
# Register models from this app
WorkflowTypeRegistry.register('MyAppWorkflow', [
{
'slug': 'my_app_model',
'label': 'My App Model',
'app_label': 'myapp',
'model_name': 'MyAppModel',
'relationships': [
{
'field_name': 'related_thing',
'label': 'Related Thing',
'type': 'fk',
'related_app': 'myapp',
'related_model': 'RelatedThing',
},
],
},
])
Example 4: Full Project Setup
# myproject/workflowconfig.py
from enezaworkflowengine.models.choices import Workflownames
class ProjectWorkflowNames(Workflownames):
"""Extended workflow names including project-specific workflows."""
SALESORDERWORKFLOW = 'SalesOrderWorkflow', 'Sales Order Workflow'
PROCUREMENTWORKFLOW = 'ProcurementWorkflow', 'Procurement Workflow'
class WorkflowTypeConfig:
"""Project-level workflow type registry."""
WORKFLOW_TYPE_REGISTRY = {
'SalesOrderWorkflow': [
{
'slug': 'sales_order',
'label': 'Sales Order',
'app_label': 'sales',
'model_name': 'SalesOrder',
'relationships': [
{
'field_name': 'customer',
'label': 'Customer',
'type': 'fk',
'related_app': 'crm',
'related_model': 'Customer',
},
{
'field_name': 'items',
'label': 'Order Items',
'type': 'm2m',
'related_app': 'inventory',
'related_model': 'InventoryItem',
},
],
},
],
'ProcurementWorkflow': [
{
'slug': 'purchase_order',
'label': 'Purchase Order',
'app_label': 'procurement',
'model_name': 'PurchaseOrder',
'relationships': [
{
'field_name': 'vendor',
'label': 'Vendor',
'type': 'fk',
'related_app': 'procurement',
'related_model': 'Vendor',
},
],
},
],
}
# myproject/settings.py
MY_PROJECT_WORKFLOWS_NAME = 'myproject.workflowconfig.ProjectWorkflowNames'
MY_PROJECT_WORKFLOWS_CONFIG = 'myproject.workflowconfig.WorkflowTypeConfig'
Troubleshooting
Issue: Custom workflow not appearing in model list
Cause 1: MY_PROJECT_WORKFLOWS_CONFIG not pointing to the correct class path
Solution: Ensure the setting uses the full dotted path: 'myproject.workflowconfig.WorkflowTypeConfig'
Cause 2: Workflow name mismatch between choices and registry
Solution: The key in WORKFLOW_TYPE_REGISTRY must exactly match a value in your Workflownames choices (case-sensitive).
Issue: Models not resolving — "No model found" errors
Cause: app_label or model_name is incorrect
Solution: Verify the values match Django's actual app and model names. Use python manage.py shell to test:
from django.apps import apps
model = apps.get_model('myapp', 'MyModel')
print(model) # Should not raise LookupError
Issue: Relationships not showing in query builder
Cause: 'relationships': [] is set (empty list disables auto-discovery)
Solution: Either populate relationships with explicit definitions, or remove the relationships key entirely to enable auto-discovery.
Issue: Changes to WORKFLOW_TYPE_REGISTRY not taking effect
Cause: The registry is cached after first load Solution: Restart the Django development server. The registry loads config once on first access and caches the result.
Issue: Cannot override framework workflow — still seeing old models
Cause: Typo in the workflow name key
Solution: The key must match exactly. Compare:
- Framework: 'InvoiceWorkflow'
- Your config: 'InvoiceWorkflow' (must be identical, case-sensitive string match)
Reference
WorkflowTypeRegistry API
| Method | Description |
|---|---|
get_allowed_models(workflow_name=None) |
Returns model config list for the workflow, or defaults |
has_workflow(workflow_name) |
Returns True if workflow is registered |
register(workflow_name, models) |
Adds/overwrites a workflow entry at runtime |
Settings
| Setting | Purpose | Required |
|---|---|---|
MY_PROJECT_WORKFLOWS_CONFIG |
Dotted path to project's WorkflowTypeConfig class | No |
MY_PROJECT_WORKFLOWS_NAME |
Dotted path to custom Workflownames choices class | No |
Files
| File | Purpose |
|---|---|
enezaworkflowengine/services/workflow_type.py |
WorkflowTypeRegistry implementation |
enezaworkflowengine/views/mixin.py |
QueryBuilderMixin — reads registry, generates filters |
enezaworkflowengine/views/querybuilder.py |
Generic query builder views |
enezaworkflowengine/models/choices.py |
Workflownames and get_getworkflow_names_choices() |
enezaworkflowengine/models/querybuilder.py |
SavedQuery model with workflow field |
myproject/workflowconfig.py |
Project-level workflow configuration (created by developer) |