Workflow Type Registry — Developer Guide

Table of Contents

  1. Overview
  2. Architecture
  3. Quick Start
  4. Registry Configuration
  5. Project-Level Injection
  6. Model Configuration Schema
  7. QueryBuilderMixin Integration
  8. Dynamic Registration API
  9. Complete Examples
  10. 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

text
┌─────────────────────────────────────────────────────────────┐
│ 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):

text
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:

text
/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:

python
{
    '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):

python
# 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:

python
# 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(), or register(). 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:

python
# myproject/workflowconfig.py (or choices.py)
from enezaworkflowengine.models.choices import Workflownames

class ProjectWorkflowNames(Workflownames):
    SALESORDERWORKFLOW = 'SalesOrderWorkflow', 'Sales Order Workflow'
    INVENTORYWORKFLOW = 'InventoryWorkflow', 'Inventory Workflow'
python
# myproject/settings.py
MY_PROJECT_WORKFLOWS_NAME = 'myproject.workflowconfig.ProjectWorkflowNames'

Model Configuration Schema

Full Schema Reference

python
{
    # === 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:

text
/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):

python
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:

python
if WorkflowTypeRegistry.has_workflow('SalesOrderWorkflow'):
    models = WorkflowTypeRegistry.get_allowed_models('SalesOrderWorkflow')

get_allowed_models(workflow_name=None)

Get allowed models for a workflow:

python
# 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:

python
# 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

python
# 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:

python
# 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

python
# 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',
                    },
                ],
            },
        ],
    }
python
# 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:

python
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)