Workflow Type Registry Architecture

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

Table of Contents

  1. Overview
  2. Component Map
  3. Registry Internals
  4. Merge and Cache Logic
  5. First-Request Data Flow
  6. 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:

  1. Framework defaults — built-in workflow types shipped with the engine.
  2. Project overrides — a project's workflowconfig/ package (or workflowconfig.py file), referenced by MY_PROJECT_WORKFLOWS_CONFIG.
  3. Runtime registration — WorkflowTypeRegistry.register() from AppConfig.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(), or register() populates cls._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_CONFIG import 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_json

The 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).