Every Django project has an apps.py in each app: most of the time, it’s three lines nobody thinks about. In Horilla CRM, that file does a lot more work. It’s where a module declares its URL prefix, tells the framework which submodules to auto-import on startup, plugs into Celery’s beat schedule, and optionally exposes its REST API, all without a single manual edit to the project’s root urls.py or settings.py.

This is possible because every Horilla app subclasses AppLauncher instead of Django’s plain AppConfig. AppLauncher overrides Django’s ready() hook to auto-register URLs, auto-import known submodules, and merge Celery schedules, turning apps.py from a formality into the single entry point for wiring a module into the platform.

If you’re adding a new module to Horilla CRM, say a deals app for tracking sales pipeline stages here’s exactly how to set up its apps.py, following the same pattern used by leads, campaigns, and every other built-in CRM app.

1. Scaffold the app as a normal Django app

horilla_crm/deals/
    __init__.py
    apps.py
    models.py
    urls.py
    registration.py   # optional, auto-imported
    signals.py        # optional, auto-imported
    menu.py           # optional, auto-imported
    dashboard.py      # optional, auto-imported

registration.py, signals.py, menu.py, and dashboard.py are plain convention — nothing requires these exact names except whatever you list in auto_import_modules in the next step.

2. Subclass AppLauncher instead of AppConfig

# horilla_crm/deals/apps.py
“””Deals app configuration.”””

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




class DealsConfig(AppLauncher):
    """Deals App Configuration"""


    default = True
    default_auto_field = "django.db.models.BigAutoField"
    name = "horilla_crm.deals"
    verbose_name = _("Deals")


    # Auto URL registration
    url_prefix = "crm/deals/"
    url_module = "horilla_crm.deals.urls"
    url_namespace = "deals"


    # Auto-imported on app ready()
    auto_import_modules = [
        "registration",
        "signals",
        "menu",
        "dashboard",
    ]


    def get_api_paths(self):
        return [
            {
                "pattern": "crm/deals/",
                "view_or_include": "horilla_crm.deals.api.urls",
                "name": "horilla_crm_deals_api",
                "namespace": "horilla_crm_deals",
            }
        ]

Only fill in what you actually need. No REST API yet? Skip get_api_paths() it defaults to an empty list. No background jobs? Skip celery_schedule_module and celery_schedule_variable entirely. No dedicated JS bundle? Skip js_files. Every field is opt-in, and AppLauncher’s ready() checks for each one before acting on it.

3. Point Django’s default app config at your class

horilla_crm/deals/init.py
default_app_config = “horilla_crm.deals.apps.DealsConfig”

Modern Django auto-detects a single AppConfig subclass in apps.py, so this line is often unnecessary — but set it explicitly if the module ever grows more than one config class.

4. Register the app in INSTALLED_APPS

# horilla/settings/horilla_apps.py

INSTALLED_APPS.extend(
    [
        "horilla_crm.accounts",
        "horilla_crm.contacts",
        "horilla_crm.leads",
        "horilla_crm.campaigns",
        "horilla_crm.deals",   # <-- new module
        ...
    ]
)

This is the one manual step AppLauncher can’t automate. Django needs INSTALLED_APPS populated before any app’s ready() runs, so this line has to exist before your apps.py logic ever executes.

5. Write the auto-imported modules

Each name in auto_import_modules maps to horilla_crm.deals.<module>. Internally, _auto_import_modules() imports each one inside a try/except, so a missing optional module just logs a warning instead of crashing the app — this is what lets you add dashboard.py later without touching apps.py again.

  • registration.py: model and permission registration, e.g. registering the model with generic list/detail/kanban views.
  • signals.py: post_save/pre_delete receivers for the app’s models.
  • menu.py: sidebar and floating-menu entries, using @sub_section_menu.register / @floating_menu.register decorators.
  • dashboard.py: dashboard widget registration, if the module contributes cards or charts to a dashboard.

6. Wire URLs and the API module

horilla_crm/deals/urls.py
from django.urls import path
from . import views

app_name = "deals"
urlpatterns = [
    path("", views.deal_view, name="deal_view"),
    ...
]

Because url_module, url_prefix, and url_namespace are set on the config class, _register_urls() appends this to the root URLconf automatically at ready() time you never touch the project’s top-level urls.py.

7. Optional: hook into conventions read by other subsystems

Some class attributes aren’t part of AppLauncher at all — they’re read via getattr by other subsystems that scan every installed app’s config. Add them only if relevant:

demo_data = {
        "files": [
            (10, "load_data/deal_stage.json"),
            (11, "load_data/deals.json"),
        ],
        "order": 3,
    }


    report_files = [
        "report_data/reports.json",
    ]

demo_data is picked up by horilla.contrib.core.views.load_data to seed sample records in order. report_files is picked up by horilla.contrib.reports.views.default_reports to register default report definitions.

This is the extensibility trick worth noting: AppLauncher only acts on the fields it explicitly reads inside ready(). Anything else you put on the config class is inert until some other piece of code decides to look for it. That’s what lets new subsystems adopt the same “declare a field on apps.py” convention without ever having to modify AppLauncher itself.

apps.py in Horilla CRM isn’t boilerplate it’s a declarative manifest for the entire module. By subclassing AppLauncher and filling in a handful of well-named class attributes, a new app gets its URLs wired, its signals and menus loaded, and its Celery schedules merged, all before a single request is served. The pattern also scales cleanly: since unused fields are simply skipped, and unrelated subsystems can read their own conventions off the same class, apps.py stays a single, predictable place to look whether you’re building your first custom module or your fiftieth.

If you’re extending Horilla CRM with a new domain object, this is the first file to get right. Everything else models, views, templates builds on top of the wiring you set up here.

Share this article