Workflow Project Configuration

Developer guide for the Enezaframework workflow engine. Part of the Workflow series.

Table of Contents

  1. Overview
  2. Step 1 — Create the workflowconfig package
  3. Single-File Alternative
  4. Step 2 — Reference It in settings.py
  5. Step 3 — Register the Workflow Name in Choices
  6. Merge Behavior
  7. Related Guides

Overview

Projects extend or override the workflow registries with a dedicated workflowconfig package, or a single workflowconfig.py file. Use the package once the registries grow: each concern lives in its own submodule and the WorkflowTypeConfig class re-exports them from __init__.py. The pattern matches other project-level injections such as MY_PROJECT_MODULES and MY_PROJECT_NUMBER_SEQUENCE_DOCUMENT: define a class in the project, then reference its dotted path from settings.py.

flowchart LR
    A["settings.MY_PROJECT_WORKFLOWS_CONFIG"] --> B["import_string(path)"]
    B --> C["workflowconfig/__init__.py<br/>WorkflowTypeConfig"]
    C --> D["type_registry.py -> WORKFLOW_TYPE_REGISTRY"]
    C --> E["state_choices.py -> WORKFLOW_STATE_CHOICES"]
    C --> F["hooks.py -> WORKFLOW_HOOKS"]
    D --> G["WorkflowTypeRegistry._load_project_registry()"]
    E --> H["services/state_choices.py"]
    F --> I["services/workflow_hooks.py"]

Step 1 — Create the workflowconfig package

Create a workflowconfig/ package in the project root, next to settings.py:

text
myproject/
    settings.py
    workflowconfig/
        __init__.py
        type_registry.py
        state_choices.py
        hooks.py

__init__.py defines WorkflowTypeConfig and re-exports the submodules, so the public API is unchanged and settings.py still points at myproject.workflowconfig.WorkflowTypeConfig:

python
# myproject/workflowconfig/__init__.py
from . import hooks, state_choices, type_registry


class WorkflowTypeConfig:
    """
    Project-level workflow type registry configuration.
    Each attribute is owned by the matching submodule.
    """
    WORKFLOW_TYPE_REGISTRY = type_registry.WORKFLOW_TYPE_REGISTRY
    WORKFLOW_STATE_CHOICES = state_choices.WORKFLOW_STATE_CHOICES
    WORKFLOW_HOOKS = hooks.WORKFLOW_HOOKS
python
# myproject/workflowconfig/type_registry.py
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': [],
        },
    ],
}
python
# myproject/workflowconfig/state_choices.py
WORKFLOW_STATE_CHOICES = {
    'IncidentWorkflow': 'myapp.models.IncidentStatus',
}
python
# myproject/workflowconfig/hooks.py
WORKFLOW_HOOKS = {
    'SalesOrderWorkflow': {
        'approved': ['myapp.workflow_hooks.on_sales_order_approved'],
    },
}

The registry dict uses the same model-config schema described in Workflow Model Configuration. A submodule may be empty when the project does not use that feature.


Single-File Alternative

A single workflowconfig.py module with the same class also works; import_string resolves the package and the file identically. Use it for small configurations that are unlikely to grow:

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 = {
        'InventoryWorkflow': [
            {
                'slug': 'inventory_item',
                'label': 'Inventory Item',
                'app_label': 'inventory',
                'model_name': 'InventoryItem',
                'relationships': [],
            },
        ],
    }

Step 2 — Reference It in settings.py

python
# myproject/settings.py

MY_PROJECT_WORKFLOWS_CONFIG = 'myproject.workflowconfig.WorkflowTypeConfig'

The loader uses import_string and reads the WORKFLOW_TYPE_REGISTRY attribute (enezaworkflowengine/services/workflow_type.py:158-164). If the setting is unset, the import fails, or the attribute is not a dict, the framework registry is used silently.


Step 3 — Register the Workflow Name in Choices

A workflow must also exist in the Workflownames choices to appear in dropdowns and to be stored on SavedQuery.workflow. Extend the framework choices and point MY_PROJECT_WORKFLOWS_NAME at your subclass.

python
# myproject/workflowconfig/__init__.py (or myproject/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'

get_getworkflow_names_choices() imports this path and warns, falling back to Workflownames, when it cannot be loaded (enezaworkflowengine/models/choices.py:31-51).

The registry key and the choice value must match exactly (case-sensitive). The Python constant name is irrelevant; the string value is what the registry looks up.


Merge Behavior

flowchart TD
    FW["_framework_registry"] --> MERGE["_load_merged_registry()"]
    PR["Project WORKFLOW_TYPE_REGISTRY"] --> MERGE
    MERGE --> SAME{"Same workflow name<br/>in both?"}
    SAME -->|Yes| WIN["Project definition replaces<br/>the framework definition entirely"]
    SAME -->|No| ADD["Project workflow added"]
    WIN --> CACHE["Cache in cls._registry"]
    ADD --> CACHE
    CACHE --> USE["Returned by get_allowed_models()"]
  • Project-level definitions take precedence. A workflow name present in both registries uses the project's list entirely (workflow_type.py:181-182).
  • The framework registry is immutable. Project config cannot edit individual framework entries — only add new names or fully override existing ones.
  • Lazy loading. The merge runs once, on first access, and is cached. Restart the server after editing the config.
  • Failures are silent. A broken path or non-dict registry falls back to framework defaults without raising (workflow_type.py:154-167).