As Horilla grows, many features need to work with different models across the system. Features such as import, export, global search, duplicate detection, approvals, reviews, workflow automation, scoring, cadences, activity logging, and calendar synchronization all need to know which models they should work with.

Instead of maintaining separate hardcoded model lists inside every feature, Horilla uses a central feature registry.

The idea is simple: each feature can ask the registry, “Which models support this feature?” The registry then returns the models that have been registered for it.

This makes the system easier to extend. When you introduce a new CRM model, you can register it for the features it needs without modifying the implementation of those individual features.

Understanding the Feature Registry

The feature registry is handled in:

horilla/registry/feature.py

Two dictionaries are at the heart of the system:

  • FEATURE_CONFIG maps a feature name to its registry key.
  • FEATURE_REGISTRY maps that registry key to the list of registered model classes.

For example, some of Horilla’s built-in features are:

FeatureRegistry Key
import_dataimport_models
export_dataexport_models
global_searchglobal_search_models

Other applications can register additional features using the same mechanism.

This gives Horilla a common way to manage feature participation across different apps.

Where Should Model Registration Live?

Each app that wants to register models should have a dedicated:

registration.py

file.

It sits alongside files such as:

models.py

signals.py

apps.py

For example, a leads app might contain:

leads/

├── models.py

├── signals.py

├── registration.py

└── apps.py

A registration can look like this:

from horilla.registry.feature import register_model_for_feature


register_model_for_feature(
    app_label="leads",
    model_name="LeadStatus",
    features=["import_data", "export_data", "global_search"],
)

The important part is that you don’t have to manually import this file from somewhere else in the project. Horilla’s AppLauncher handles that automatically.

How Horilla Automatically Loads registration.py

This automatic loading is handled by AppLauncher.

When an application starts, AppLauncher checks its auto_import_modules list and imports the modules listed there. The default mechanism supports modules such as:

  • registration
  • signals
  • menu
  • dashboard
  • scheduler

When “registration” is included, the registration code runs during application startup.

For example:

class LeadConfig(AppLauncher):
    name = "leads"
    auto_import_modules = ["registration", "signals", "menu"]

Once this is configured, calls to register_model_for_feature() or register_feature() inside registration.py are executed automatically.

This is an important part of the setup. You can write the registration correctly, but if the module isn’t included in auto_import_modules, the registration won’t be loaded automatically.

Registering a Model for Existing Features

The main API for registering a model is:

register_model_for_feature()

The preferred approach is to provide the features as a list:

from horilla.registry.feature import register_model_for_feature


register_model_for_feature(
    app_label="my_app",
    model_name="Quote",
    features=["import_data", "export_data", "global_search"],
)

This tells Horilla that the Quote model should participate in import, export, and global search.

You can also pass the model class directly:

from .models import Quote


register_model_for_feature(
    model_class=Quote,
    features=["global_search"],
)

There is also a legacy boolean style:

register_model_for_feature(
    app_label="my_app",
    model_name="Quote",
    import_data=True,
    export_data=True,
    global_search=True,
)

The list-based style is the preferred modern approach.

Registering a Model for All Features

For major CRM entities, you may want the model to participate in most available features.

That’s where:

all=True

becomes useful.

For example:

register_model_for_feature(
    app_label="leads",
    model_name="Lead",
    all=True,
    features=[
        "duplicate_models",
        "approval_models",
        "reviews_models",
        "workflow_models",
        "scoring",
    ],
)

all=True enables the core features, while the features list can be used to explicitly add other features that don’t automatically apply to every model.

You can also exclude something you don’t want:

register_model_for_feature(
    app_label="my_app",
    model_name="Quote",
    all=True,
    exclude=["export_data"],
)

This is useful when a model generally behaves like a full CRM entity but has a specific feature that should remain disabled.

Registering Multiple Models at Once

If several models need exactly the same features, registering them individually can become repetitive.

Horilla provides:

register_models_for_feature()

For example:

from horilla.registry.feature import register_models_for_feature

result = register_models_for_feature(
models=[
("my_app", "Quote"),
("my_app", "QuoteLine"),
],
features=["import_data", "export_data"],
)

The function returns information about which registrations succeeded or failed, along with the total number of models processed.

This is particularly convenient when several related models share the same feature requirements.

Creating a New Feature

The registry isn’t only for registering models with existing features. You can also use it when creating a completely new feature.

Suppose you are building a scoring system. You can define a feature such as:

register_feature(
    "scoring",
    "scoring_models",
    include_models=[
        ("leads", "lead"),
        ("opportunities", "opportunity"),
        ("accounts", "account"),
        ("contacts", "contact"),
    ],
)

The register_feature() function defines the feature and determines which models can participate in it.

Several arguments control how the feature behaves:

ArgumentPurpose
feature_nameThe name consumers use to find the feature
registry_keyThe key used inside FEATURE_REGISTRY
auto_register_allAutomatically includes models registered with all=True
include_modelsExplicitly specifies which models are included
exclude_modelsPrevents specific models from being included
exclude_app_labelPrevents models from particular apps from being automatically included

These options allow a feature to be either very selective or broadly available.

Selective vs Broad Features

There are two common approaches when creating a feature.

Selective Feature

Some features should only apply to models that explicitly opt in.

For example:

register_feature(
    "duplicate_data",
    "duplicate_models",
    auto_register_all=False,
)

This means models need to be explicitly registered for the feature.

Broad Feature

Other features make sense for most first-class CRM entities.

For example:

register_feature(
    "workflow",
    "workflow_models",
    auto_register_all=True,
)

In this case, models registered with all=True can automatically participate in the workflow feature.

This distinction is useful when designing a new feature. You should think about whether the feature should be opt-in or automatically available to the main CRM models.

What Happens if Registration Order Changes?

One useful part of the registry design is that registration order doesn’t have to be perfect.

A model might attempt to register for a feature before that feature itself has been defined.

Horilla handles this using FEATURE_PENDING_MODELS. The model registration is temporarily stored and processed when the corresponding register_feature() call eventually runs.

That makes the registration system more flexible and reduces the need to carefully control import order between applications.

How Features Use the Registry

The main benefit of the registry becomes clear when looking at the feature implementations themselves.

Instead of maintaining something like:

searchable_models = [
    Lead,
    Account,
    Contact,
    Opportunity,
]

a feature can retrieve its models from the registry:

from horilla.registry.feature import FEATURE_CONFIG, FEATURE_REGISTRY

registry_key = FEATURE_CONFIG[“global_search”]

searchable_models = FEATURE_REGISTRY[registry_key]

The feature doesn’t need to know which applications contain the models. It simply asks the registry for the models registered for that feature.

This is what makes the approach scalable.

When a new model is added, the feature implementation doesn’t need to be changed. The new model only needs to register itself.

Adding a New CRM Module: A Practical Example

Let’s say you’re adding a new quotes application with two models:

Quote

QuoteStatus

You want Quote to behave like the other major CRM entities.

Your quotes/registration.py could look like:

from horilla.registry.feature import register_model_for_feature
from horilla.contrib.cadences.registration import register_cadence_tab

register_model_for_feature(
    app_label="quotes",
    model_name="Quote",
    all=True,
    features=[
        "duplicate_models",
        "approval_models",
        "reviews_models",
        "workflow_models",
        "scoring",
    ],
)


register_model_for_feature(
    app_label="quotes",
    model_name="QuoteStatus",
    features=[
        "import_data",
        "export_data",
        "global_search",
    ],
)


register_cadence_tab(
    app_label="quotes",
    model_name="Quote",
    url_prefix="quote-cadences-tab/<int:pk>/",
    url_name="quote_cadences_tab",
)

Then configure the application:

from horilla.apps import AppLauncher
from horilla.utils.translation import gettext_lazy as _


class QuotesConfig(AppLauncher):
    default_auto_field = "django.db.models.BigAutoField"
    name = "quotes"
    verbose_name = _("Quotes")
    url_prefix = "quotes/"
    url_module = "quotes.urls"
    url_namespace = "quotes"
    auto_import_modules = ["registration", "signals", "menu"]

With this setup, the Quote model can participate in the registered cross-cutting features without modifying each feature application individually.

You Don’t Have to Enable Everything

A common mistake when adding a new model is to enable every feature immediately.

That isn’t always necessary.

You can start with the basics:

register_model_for_feature(
    app_label="quotes",
    model_name="Quote",
    features=[
        "import_data",
        "export_data",
        "global_search",
    ],
)

As the module becomes more mature, you can add features based on actual requirements:

  • all=True when the model becomes a first-class CRM entity
  • duplicate_models when duplicate detection is needed
  • approval_models when approval workflows are required
  • reviews_models when review functionality is relevant
  • workflow_models when workflow automation is needed
  • scoring when scoring rules should apply
  • register_cadence_tab when cadence functionality is required
  • register_feature() when the application introduces a completely new capability

This incremental approach keeps the initial implementation simple while still leaving room for the model to grow.

Why This Approach Works Well

The feature registry gives Horilla a clean separation between feature implementation and feature participation.

A feature doesn’t need to maintain a list of every model it supports. Instead, applications declare their participation through registration.py.

That has several practical advantages:

  • Less Hardcoding:Feature implementations don’t need to contain large lists of CRM models.
  • Easier Extension:Adding a new model usually means adding a registration rather than modifying multiple feature applications.
  • Better Separation of Responsibilities:The feature knows what it does, while the registration determines where it applies.
  • Centralized Discovery:Features can consistently retrieve participating models through FEATURE_CONFIG and FEATURE_REGISTRY.
  • Easier Maintenance:As Horilla gains more applications and cross-cutting features, registrations provide a predictable place to manage those relationships.

Registering a model in Horilla is more than simply setting a flag. You need to decide which features the model should support and whether those features should be enabled individually or through all=True.

For existing features, the main tool is:

register_model_for_feature()

For defining a completely new capability, use:

register_feature()

And for the registration system to be loaded automatically, make sure your application’s apps.py includes:

auto_import_modules = [“registration”, “signals”, “menu”]

Once that is in place, Horilla’s feature registry takes care of connecting the model with the appropriate cross-cutting features.

The result is a system where adding a new CRM model doesn’t require repeatedly editing global-search code, import/export code, workflow code, approval code, or other feature implementations. The model simply declares what it supports, and the registry handles the rest.

Share this article