# Why does my coding agent miss Django signal receivers?

> Rule AR-31: Connect Django signals with an explicit sender, and keep the receivers in one signals.py per app, imported in AppConfig.ready().

A receiver has no caller in the source. Django calls it when a model is saved, so a search for who uses the function finds only its definition. Name the sender in the decorator, put every receiver of an app in one file, and import that file from `ready()`. Then a reader, a type checker or an agent can answer two questions from the source alone: which model triggers this function, and where does it get connected.

## Why it happens
Django's signal system does its job well. It decouples the code that saves a model from the code that reacts, `send()` runs receivers in a fixed order, and the `@receiver` decorator keeps the registration next to the function. The cost is that the link between sender and receiver lives in Django's registry, not in a call a tool can follow.

The signals topic in the Django docs says what the registry does with each choice:

- **Where receivers live.** "Strictly speaking, signal handling and registration code can live anywhere you like, although it's recommended to avoid the application's root module and its `models` module to minimize side-effects of importing code." Receivers belong in a `signals` submodule, connected in `ready()`: "If you're using the `receiver()` decorator, import the `signals` submodule inside `ready()`, this will implicitly connect signal receivers."
- **What `sender` does.** For `@receiver(pre_save, sender=MyModel)`, the docs say the function "will only be called when an instance of `MyModel` is saved." Leave `sender` out and the receiver is connected to every sender of that signal, so the function has to filter at runtime.
- **When `ready()` runs.** The applications reference says it is "called as soon as the registry is fully populated", the third and final stage of initialization. It also warns that `ready` "might be called more than once" in tests that change installed apps, so registration should be idempotent.
- **Order.** "All of the signal's receiver functions are called one at a time, in the order they were registered." Registration order follows import order, so a receiver connected as a side effect of importing `models.py` is placed by whatever imports `models.py` first.
- **Errors.** `send()` "does not catch any exceptions raised by receivers; it simply allows errors to propagate." A receiver that breaks after a refactor fails the `save()` call that triggered it. [CH-01](https://beta.llmgraph.in/agent-readable/ch-01.md) shows that failure end to end.

The agent's problem follows from this. Asked to change `notify_warehouse(order)`, it searches for callers and finds none in the project. It has no way to know that every `Order.save()` calls it, or that a receiver without `sender` also runs for models the agent never opened.

## Bad and good
An order service notifies the warehouse after an order is saved.

```python
# bad: orders/models.py
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver


class Order(models.Model):
    status = models.CharField(max_length=20)


@receiver(post_save)                      # connected for every model's save
def notify_warehouse(sender, instance, created, **kwargs):
    if sender.__name__ != "Order":        # runtime filter by name
        return
    if created:
        warehouse.send(instance.pk)
```

Three things hide here. The receiver is connected to every model's `post_save`. The filter compares a class name as a string. The receiver sits in `models.py`, which the Django docs say to avoid for registration code. An agent that renames `Order` finds the string `"Order"` only by text search, and the receiver quietly stops matching.

```python
# good: orders/signals.py
from typing import Any

from django.db.models.signals import post_save
from django.dispatch import receiver

from orders import warehouse
from orders.models import Order


@receiver(post_save, sender=Order)
def notify_warehouse(
    sender: type[Order], instance: Order, created: bool, **kwargs: Any
) -> None:
    if created:
        warehouse.send(instance.pk)
```

```python
# good: orders/apps.py
from django.apps import AppConfig


class OrdersConfig(AppConfig):
    name = "orders"

    def ready(self) -> None:
        # Importing the module connects the @receiver functions in it.
        from orders import signals  # noqa: F401
```

`models.py` no longer mentions signals. `sender=Order` is a reference to the class, so a rename tool updates it and a search for `Order` lists the receiver file. All receivers of the app are in one file, so "what reacts to saves in this app" is one file to open.

## Trade-offs and when the other way is fine
- **The decorator keeps things short.** `@receiver` is the most readable form Django offers. The rule asks for one more argument and one more file, not a different mechanism.
- **A receiver with no sender is right for cross-cutting work.** Audit logging for every model, or a metrics hook on `request_finished`, should not name a sender. Keep those in a `signals.py` that says so in a comment.
- **Signals are the wrong tool when you own both sides.** If a view saves an order and sends one email, calling `send_confirmation(order)` from the view is a plain call that every tool follows. Use signals when the sender can't know about the receiver, as with third-party apps.
- **`ready()` adds a file and a few lines per app.** That is the whole cost.
- **Importing `signals` inside `ready()` hides it from a plain top-of-file import scan.** The `noqa: F401` comment and the name `signals` are the convention. A comment in `apps.py` naming the receivers' file helps.

## Enforce it
| Tool | Setting | What it catches | Checked |
|---|---|---|---|
| ruff | `DJ013` non-leading-receiver-decorator (select the `DJ` family; the rule is not marked preview, and ruff's default rule set lists no `DJ` rule, so it is off until selected: https://docs.astral.sh/ruff/default-rules/) | a decorator placed above `@receiver`; the docs say `@receiver` "does not return a wrapped function", so receivers listed after another decorator "will not be connected" | 2026-10-30 |
| Django system check | `signals.E001` (run `python manage.py check`) | a receiver connected with a string sender `app_label.Model` where the app isn't installed or has no such model | 2026-10-30 |
| Django | `dispatch_uid` on `connect()` | duplicate registration of a bound-method receiver; module-level functions are registered once already | 2026-10-30 |
| Django | `weak=False` on `connect()` | a local-function receiver being garbage collected, since receivers are stored as weak references by default | 2026-10-30 |

No ruff or Django check in the pages read flags a `@receiver` without `sender`. That one is a review rule, and the AGENTS.md block below is how an agent learns it.

## Rule block for AGENTS.md / CLAUDE.md
```markdown
## Django signals
- Every @receiver names its sender: @receiver(post_save, sender=Order). A receiver with no sender needs a comment saying why it listens to all senders.
- Receivers live in <app>/signals.py. Nothing in models.py or the app's __init__.py connects signals.
- apps.py imports the signals module inside AppConfig.ready().
- @receiver is the first decorator above the function.
- Before changing a receiver's signature or deleting it, search for the signal name and the sender class. Callers of the receiver itself are not in the source.
- Don't call handler functions by names built from data (getattr, f-strings). Use an explicit match (AR-07, AR-08).
```

## Edge cases
- **Bound-method receivers.** Connecting a method of an instance that is created again registers another receiver. Pass `dispatch_uid` (the docs: "The receiver will only be bound to the signal once for each unique `dispatch_uid` value").
- **Receivers defined inside a function.** The docs: "if your receiver is a local function, it may be garbage collected." Define it at module level, or pass `weak=False`.
- **String senders.** The applications reference allows `pre_save.connect(receiver, sender="app_label.MyModel")` inside `ready()`. Use it to avoid an import cycle between apps; the `signals.E001` check covers a wrong label.
- **A plain `"orders"` entry in `INSTALLED_APPS`.** For an app listed by package name, Django uses the one `AppConfig` subclass it finds in `apps.py` ("if Django finds exactly one `AppConfig` subclass in the `apps.py` submodule, it uses that configuration"), and then "runs the `ready()` method of each application configuration". With no subclass, the base `AppConfig` is used and its `ready()` does nothing; with several, the one with `default = True` is chosen. https://docs.djangoproject.com/en/stable/ref/applications/
- **Async receivers.** The docs say receivers may be `async def`, and "an asynchronous receiver registered before a synchronous receiver may be executed after the synchronous receiver", so don't rely on registration order across the two.
- **Tests.** `ready()` may run more than once during testing. Module-level function receivers connected by import are safe.
- **Dispatch by status inside a receiver.** A `post_save` receiver that routes by a status field belongs to AR-07 and AR-08 as well. See CH-01.

## What LLMGRAPH shows
A function connected with the receiver decorator prints 'no calls to it found in this repo'; LLMGRAPH does not mark it as an entry point.

## References
Django (fetched 2026-10-30 from the stable docs):
- Signals topic (where to put receivers, `ready()`, `@receiver(sender=...)`, `dispatch_uid`, weak references, call order, `send()` errors): https://docs.djangoproject.com/en/stable/topics/signals/
- Applications reference (`AppConfig.ready()`, initialization stages, string sender labels, `ready()` more than once): https://docs.djangoproject.com/en/stable/ref/applications/
- System check framework (`signals.E001`): https://docs.djangoproject.com/en/stable/ref/checks/

Ruff (fetched 2026-10-30):
- DJ013: https://docs.astral.sh/ruff/rules/django-non-leading-receiver-decorator/ · rule list: https://docs.astral.sh/ruff/rules/

Chimera: [CH-01](https://beta.llmgraph.in/agent-readable/ch-01.md).

## Changelog
- 2026-10-30: draft 1.
- 2026-10-30: docs checks resolved
- 2026-10-30: remaining checks closed (claims reduced to documented behaviour)

## Related

- [AR-07: Why does my coding agent miss calls made through getattr?](https://beta.llmgraph.in/agent-readable/ar-07.md)
- [AR-08: How do I write dispatch that type checkers can follow?](https://beta.llmgraph.in/agent-readable/ar-08.md)
- [AR-25: What to check before changing a function signature?](https://beta.llmgraph.in/agent-readable/ar-25.md)
- [AR-32: Do string model references hide dependencies in Django?](https://beta.llmgraph.in/agent-readable/ar-32.md)
- [CH-01: Why does my Django signal break after a refactor?](https://beta.llmgraph.in/agent-readable/ch-01.md)

## Chimeras that use this rule

- [CH-01: Why does my Django signal break after a refactor?](https://beta.llmgraph.in/agent-readable/ch-01.md)

Last verified 2026-10-30
