Workflow Project Configuration
Developer guide for the Enezaframework workflow engine. Part of the Workflow series.
Table of Contents
- Overview
- Step 1 — Create the workflowconfig package
- Single-File Alternative
- Step 2 — Reference It in settings.py
- Step 3 — Register the Workflow Name in Choices
- Merge Behavior
- 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:
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:
# 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
# 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': [],
},
],
}
# myproject/workflowconfig/state_choices.py
WORKFLOW_STATE_CHOICES = {
'IncidentWorkflow': 'myapp.models.IncidentStatus',
}
# 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:
# 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
# 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.
# 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'
# 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).