Run Side Effects After Commit in Rails Callbacks

Catches emails, jobs, and API calls fired from after_save or inside transactions that can roll back, and dirty checks that stop working after save.

What it flags

Flaggedapp/models/order.rb
class Order < ApplicationRecord
  belongs_to :customer
  has_many :line_items, dependent: :destroy

  after_create :send_confirmation_and_reserve_stock # runs before the transaction commits
  after_save :notify_shipped, if: :shipped_at_changed?

  private

  def send_confirmation_and_reserve_stock
    OrderMailer.confirmation(self).deliver_later
    WarehouseClient.reserve(order_id: id)
  end

  def notify_shipped
    ShipmentMailer.shipped(self).deliver_later
  end
end
Passesapp/models/order.rb
class Order < ApplicationRecord
  belongs_to :customer
  has_many :line_items, dependent: :destroy

  after_create_commit :send_confirmation_and_reserve_stock
  after_save_commit :notify_shipped, if: :saved_change_to_shipped_at?

  private

  def send_confirmation_and_reserve_stock
    OrderMailer.confirmation(self).deliver_later
    ReserveStockJob.perform_later(id)
  end

  def notify_shipped
    ShipmentMailer.shipped(self).deliver_later
  end
end

Why it matters

Callbacks such as after_create run inside the database transaction, so an email or API call can go out for a record that is then rolled back. A job enqueued before the commit can start on another worker before the row is visible, then fail with RecordNotFound or read stale data. In tests, jobs run inline or are only recorded by the test adapter, so the race between the commit and the worker never shows up.

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.

## Run Side Effects After Commit in Rails Callbacks

Applies to `**/app/**/*.rb`, `**/lib/**/*.rb`.

Review Active Record callbacks, models, and service objects for side effects that run before the database transaction commits.

Flag changes that:
- Send email (`deliver_now`, `deliver_later`), enqueue jobs, publish messages, or call external APIs from `after_save`, `after_create`, `after_update`, or `after_destroy` instead of `after_commit`, `after_create_commit`, `after_update_commit`, or `after_save_commit`.
- Enqueue a job, send an email, or call an external API inside a `transaction` block. A later failure rolls back the rows but not the side effect, and a fast worker can start before the commit and not find the record.
- Make HTTP requests or other slow I/O in callbacks that run inside the transaction, holding row locks and a database connection for the length of the call.
- Register `after_create_commit` and `after_update_commit` with the same method name. Only the last registration takes effect; use `after_save_commit` or `after_commit on: [:create, :update]`.
- Check `status_changed?`, `changes`, or `status_was` in `after_save` or `after_commit`, which already reflect the saved state; use `saved_change_to_status?`, `saved_changes`, or `status_before_last_save`.
- Rely on a callback for a required side effect while the same change writes through `update_all`, `update_columns`, `insert_all`, or `upsert_all`, which skip callbacks.

Do not flag:
- Jobs and `deliver_later` calls enqueued through Active Job when `enqueue_after_transaction_commit` is enabled for the app or the job, which defers the enqueue until the transaction commits.
- Writes to a transactional outbox table that a separate process publishes after commit.
- Callbacks that only update the record's own attributes or other rows in the same database.

Checks files matching **/app/**/*.rb, **/lib/**/*.rb.

More Ruby on Rails agents

See all Ruby on Rails 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.

Catch this in your code