Workflow Type Registry Architecture
Developer guide for the Enezaframework workflow engine. Part of the Workflow series.
Table of Contents
- Overview
- Component Map
- Registry Internals
- Merge and Cache Logic
- First-Request Data Flow
- Related Guides
Overview
WorkflowTypeRegistry (enezaworkflowengine/services/workflow_type.py:5) is the single source of truth mapping a Workflownames choice value to the Django models its condition queries may use. Instead of every view hardcoding model discovery, views ask the registry for a list of model configuration dicts and turn each into filter definitions.
The registry supports three layers:
- Framework defaults — built-in workflow types shipped with the engine.
- Project overrides — a project's
workflowconfig/package (orworkflowconfig.pyfile), referenced byMY_PROJECT_WORKFLOWS_CONFIG. - Runtime registration —
WorkflowTypeRegistry.register()fromAppConfig.ready()or a signal.
Project definitions take precedence over framework definitions for the same workflow name.
Component Map
flowchart TD
subgraph Engine["enezaworkflowengine"]
REG["WorkflowTypeRegistry<br/>services/workflow_type.py:5"]
FW["_framework_registry<br/>line 36"]
DEFAULTS["_default_models (User, Entity)<br/>line 104"]
MIXIN["QueryBuilderMixin<br/>views/mixin.py:38"]
VIEWS["Generic query views<br/>views/querybuilder.py"]
CHOICES["Workflownames<br/>models/choices.py:24"]
SQ["SavedQuery<br/>models/querybuilder.py:16"]
end
subgraph Project["Project"]
CFG["workflowconfig/<br/>WorkflowTypeConfig.WORKFLOW_TYPE_REGISTRY"]
SETTINGS["settings.py<br/>MY_PROJECT_WORKFLOWS_CONFIG"]
CHOICESET["MY_PROJECT_WORKFLOWS_NAME"]
end
FW --> REG
CFG --> REG
SETTINGS --> REG
REG --> MIXIN
MIXIN --> VIEWS
CHOICES --> REG
CHOICESET --> CHOICES
VIEWS --> SQ
DEFAULTS --> REG| Piece | Location | Role |
|---|---|---|
WorkflowTypeRegistry |
enezaworkflowengine/services/workflow_type.py:5 |
Maps workflow names to model configs. |
_framework_registry |
workflow_type.py:36 |
Built-in workflow type definitions. |
_default_models |
workflow_type.py:104 |
Fallback models (User, Entity). |
_load_project_registry() |
workflow_type.py:137 |
Imports the project config class. |
_load_merged_registry() |
workflow_type.py:170 |
Merges framework + project, caches result. |
get_allowed_models() |
workflow_type.py:189 |
Public lookup with fallback. |
QueryBuilderMixin |
enezaworkflowengine/views/mixin.py:38 |
Reads the registry, generates filters and aggregate options. |
BuildQueryView / SaveQueryView |
enezaworkflowengine/views/querybuilder.py:55, :79 |
Generic builder and save endpoints. |
Workflownames |
enezaworkflowengine/models/choices.py:24 |
Choice values used as registry keys. |
get_getworkflow_names_choices() |
models/choices.py:31 |
Loads project choices via MY_PROJECT_WORKFLOWS_NAME. |
Registry Internals
Each registry entry is workflow_name -> [model_config, ...]. A model config is a dict with slug, label, app_label, model_name, and an optional relationships list. See Workflow Model Configuration.
The framework ships these keys (_framework_registry, workflow_type.py:36-99):
| Workflow name | Primary model | App |
|---|---|---|
PurchaseOrderWorkflow |
PurchaseOrder |
purchasing |
InvoiceWorkflow |
Frameworktestdocument |
frameworkapp |
CreditCardWorkflow |
CreditCardTransaction |
accounting |
ExpenseWorkflow |
Expense |
accounting |
LeaveApplicationWorkflow |
LeaveApplication |
hr |
When a workflow name is unknown (or None), get_allowed_models() returns _default_models: a User config and an Entity config (workflow_type.py:104-134).
Merge and Cache Logic
flowchart TD
CALL["get_allowed_models(workflow_name)"] --> MERGE["_load_merged_registry()"]
MERGE --> CACHED{"_registry cached and<br/>_settings_loaded?"}
CACHED -->|Yes| RETURN["Return cached registry"]
CACHED -->|No| COPY["deepcopy(_framework_registry)"]
COPY --> LOAD["_load_project_registry()"]
LOAD --> IMP{"settings.MY_PROJECT_WORKFLOWS_CONFIG set?"}
IMP -->|No| STORE["Cache framework-only registry"]
IMP -->|Yes| CLASS["import_string(config_path)"]
CLASS --> ATTR{"class has WORKFLOW_TYPE_REGISTRY dict?"}
ATTR -->|No| STORE
ATTR -->|Yes| OVERRIDE["merged[name] = deepcopy(project models)<br/>for each project workflow"]
OVERRIDE --> STORE
STORE --> RETURN
RETURN --> LOOK{"workflow_name?"}
LOOK -->|Yes| GET["registry.get(name, _default_models)"]
LOOK -->|No| DEFAULTS["_default_models"]Rules to remember:
- Project wins entirely. If a workflow name exists in both registries, the project's list replaces the framework's list for that name (
workflow_type.py:181-182). Framework entries cannot be partially merged. - Merged once. The first call to
get_allowed_models(),has_workflow(), orregister()populatescls._registry; later calls reuse it (workflow_type.py:175-176). Edit config and restart the server to pick up changes. - Deep copies. Both the framework registry and project models are
deepcopy-ed so runtime mutation cannot corrupt the class-level defaults. - Errors are swallowed. A bad
MY_PROJECT_WORKFLOWS_CONFIGimport or a non-dict registry is ignored and the framework registry is used (workflow_type.py:154-167). This makes misconfiguration silent — see Troubleshooting.
First-Request Data Flow
sequenceDiagram
actor U as User
participant V as BuildQueryView
participant M as QueryBuilderMixin
participant R as WorkflowTypeRegistry
participant S as settings
participant P as Project config class
U->>V: GET build?workflow_name=ExpenseWorkflow
V->>M: get_allowed_models()
M->>M: get_workflow_name() -> GET/POST param, else view attr
M->>R: get_allowed_models(workflow_name)
R->>R: _load_merged_registry()
R->>S: get MY_PROJECT_WORKFLOWS_CONFIG
S-->>R: dotted path (if set)
R->>P: import_string(path)
P-->>R: WORKFLOW_TYPE_REGISTRY
R->>R: merge + cache
R-->>M: model config list
M-->>V: allowed_models
V->>V: get_model_filters() + get_aggregate_options() per model
V-->>U: query_builder.html with filters_by_model_jsonThe view builds filters_by_model for every allowed model, then attaches an aggregates structure and serializes the whole map into filters_by_model_json for the template (enezaworkflowengine/views/querybuilder.py:68-74).
Key call sites:
QueryBuilderMixin.get_workflow_name()— resolves the workflow name from the request or the view attribute (views/mixin.py:51-65).QueryBuilderMixin.get_allowed_models()— delegates to the registry (views/mixin.py:67-81).BuildQueryView.get_context_data()— assembles the builder context (views/querybuilder.py:58-76).