Make Django Writes Atomic and Run Side Effects on Commit
Flags related Django writes outside transaction.atomic and Celery tasks, emails, or webhooks sent before the transaction commits.
What it flags
from django.db import transaction
from .models import Order, OrderItem
from .tasks import send_order_confirmation
@transaction.atomic
def place_order(user, cart):
order = Order.objects.create(user=user, total=cart.total)
items = [
OrderItem(order=order, product_id=line.product_id, quantity=line.quantity)
for line in cart.lines.all()
]
OrderItem.objects.bulk_create(items)
# A worker can pick this up before commit and find no order
send_order_confirmation.delay(order.pk)
cart.lines.all().delete()
return orderfrom functools import partial
from django.db import transaction
from .models import Order, OrderItem
from .tasks import send_order_confirmation
@transaction.atomic
def place_order(user, cart):
order = Order.objects.create(user=user, total=cart.total)
items = [
OrderItem(order=order, product_id=line.product_id, quantity=line.quantity)
for line in cart.lines.all()
]
OrderItem.objects.bulk_create(items)
cart.lines.all().delete()
transaction.on_commit(partial(send_order_confirmation.delay, order.pk))
return orderWhy it matters
A Celery task queued inside transaction.atomic can start before the commit and find no row, or run after a rollback and email a customer about an order that does not exist. When ATOMIC_REQUESTS is on, every view runs inside a transaction, so the same bug appears in code that never calls atomic() itself. Tests rarely catch it: with CELERY_TASK_ALWAYS_EAGER the task runs inline on the same database connection and sees the uncommitted row, and TestCase never commits, so on_commit callbacks do not run unless the test uses captureOnCommitCallbacks.
Agent instructions
cubic checks every pull request against these instructions. Paste them into AGENTS.md or Cursor rules and your AI coding tool follows them while it writes code.
AGENTS.md
Add this section to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.
## Make Django Writes Atomic and Run Side Effects on Commit Applies to `**/*.py`. Review Django code that writes to the database for missing transactions and for side effects that run before the data they depend on is committed. Flag changes that: - Perform several related writes (`create()`, `save()`, `update()`, `delete()`, `bulk_create()`) that must succeed or fail together without `transaction.atomic`, when `ATOMIC_REQUESTS` does not cover the code path. - Call Celery `.delay()` or `.apply_async()`, `send_mail()` or `EmailMessage.send()`, or publish webhooks and events inside an atomic block or an `ATOMIC_REQUESTS` view instead of through `transaction.on_commit()`. - Make slow or irreversible external calls, such as payment charges or third-party HTTP requests, inside `transaction.atomic`, which holds locks during network I/O and cannot be undone by a rollback. - Read a value, change it in Python, and `save()` it back (balances, stock counts, counters) without an `F()` expression or `select_for_update()` inside `transaction.atomic`. - Catch `IntegrityError` or `DatabaseError` inside an atomic block without wrapping the failing query in its own nested `transaction.atomic()`, which leaves the outer transaction unusable. Do not flag: - A single ORM call such as one `save()`, `QuerySet.update()`, `delete()`, or `bulk_create()`, which Django applies atomically on its own. - Side effects already deferred with `transaction.on_commit()` or Celery's `delay_on_commit()`. - Logging and metrics calls, which are harmless if the transaction rolls back.
Checks files matching **/*.py.
More Django agents
See all Django code review standards
Run this agent on every pull request
cubic reviews every pull request against the agents you turn on and comments on the lines that break them.